소개
이 자습서에서는 리포지토리로 푸시되는 새 코드에 대한 테스트를 실행하는 CI(연속 통합) 서버를 빌드하는 방법을 보여 줍니다. 이 자습서에서는 GitHub의 REST API를 사용하여 check_run
및 check_suite
웹후크 이벤트를 수신하고 응답하는 서버 역할을 하도록 GitHub App을(를) 빌드하고 구성하는 방법을 보여 줍니다.
이 자습서에서는 앱을 개발하는 동안 컴퓨터 또는 codespace를 서버로 사용합니다. 앱이 프로덕션 사용을 위해 준비되면 앱을 전용 서버에 배포해야 합니다.
이 자습서에서는 Ruby를 사용하지만, 서버에서 실행할 수 있는 모든 프로그래밍 언어를 사용할 수 있습니다.
이 자습서는 두 부분으로 나뉩니다.
- 1부에서는 GitHub의 REST API를 사용하여 CI 서버에 대한 프레임워크를 설정하고, 리포지토리가 새로 푸시된 커밋을 수신할 때 CI 테스트에 대한 새 검사 실행을 만들고, 사용자가 GitHub에서 해당 작업을 요청할 때 검사 다시 실행하는 방법을 알아봅니다.
- 2부에서는 CI 서버에 Linter 테스트를 추가하여 CI 테스트에 기능을 추가합니다. 끌어오기 요청의 검사 및 변경된 파일 탭에 표시되는 주석을 만들고 끌어오기 요청의 검사 탭에서 "이 문제 해결" 단추를 노출하여 Linter 권장 사항을 자동으로 수정합니다.
CI(연속 통합) 정보
CI는 공유 리포지토리에 코드를 자주 커밋해야 하는 소프트웨어 사례입니다. 코드를 자주 커밋하면 오류가 더 빨리 발생하며 개발자가 오류의 원인을 찾을 때 디버그해야 하는 코드의 양이 줄어듭니다. 또한 코드가 자주 업데이트되면 소프트웨어 개발 팀의 여러 구성원의 변경 내용을 보다 쉽게 병합할 수 있습니다. 이는 코드를 작성하는 데 더 많은 시간을 사용하고 오류를 디버그하거나 병합 충돌을 해결하는 데 더 적은 시간을 사용할 수 있는 개발자에게 유용합니다.
CI 서버는 코드 Linter(스타일 양식 확인), 보안 검사, 코드 검사 및 리포지토리의 새 코드 커밋에 대한 기타 검사와 같은 CI 테스트를 실행하는 코드를 호스트합니다. CI 서버는 스테이징 또는 프로덕션 서버에 코드를 빌드하고 배포할 수도 있습니다. GitHub App을(를) 사용하여 만들 수 있는 CI 테스트 유형의 예시는 GitHub Marketplace에서 사용할 수 있는 연속 통합 앱을 참조하세요.
검사 정보
GitHub의 REST API를 사용하면 리포지토리의 각 코드 커밋에 대해 자동으로 실행되는 CI 테스트(검사)를 설정할 수 있습니다. API는 GitHub의 끌어오기 요청의 검사 탭의 각 검사에 대한 자세한 정보를 보고합니다. 리포지토리의 검사를 사용하여 코드 커밋으로 인해 오류가 발생하는 시기를 확인할 수 있습니다.
검사에는 검사 실행, 검사 도구 모음 및 커밋 상태 포함됩니다.
- _검사 실행_은 커밋에서 실행되는 개별 CI 테스트입니다.
- _검사 도구 모음_은 검사 실행 그룹입니다.
- _커밋 상태_는 커밋의 상태를 표시하며(예:
error
,failure
,pending
또는success
) GitHub의 끌어오기 요청에 표시됩니다. 검사 도구 모음과 검사 실행 모두 커밋 상태를 포함합니다.
GitHub은(는) 기본 흐름을 사용하여 리포지토리에서 새 코드 커밋에 대한 check_suite
이벤트를 자동으로 만듭니다. 기본 설정은 변경할 수 있습니다. 자세한 내용은 "검사 도구 모음에 대한 REST API 엔드포인트"을(를) 참조하세요. 다음은 기본 흐름의 작동 방식입니다.
- 누군가가 리포지토리에 코드를 푸시하면 GitHub은(는)
checks:write
권한이 있는 리포지토리에 설치된 모든 GitHub Apps에 작업requested
와 함께check_suite
이벤트를 자동으로 보냅니다. 이 이벤트를 통해 앱은 코드가 리포지토리로 푸시되었으며 GitHub이(가) 자동으로 새 검사 도구 모음을 생성했음을 알 수 있습니다. - 이 이벤트를 수신한 앱은 해당 도구 모음에 검사 실행 추가가 가능합니다.
- 검사 실행에는 특정 코드 줄에 표시되는 주석이 포함될 수 있습니다. 주석은 검사 탭에 표시됩니다. 끌어오기 요청의 일부인 파일에 대한 주석을 생성하면 주석이 변경된 파일 탭에도 표시됩니다. 자세한 내용은 "검사 실행에 대한 REST API 엔드포인트"의
annotations
개체를 참조하세요.
검사에 대한 자세한 내용은 "검사에 대한 REST API 엔드포인트" 및 "REST API를 사용하여 검사 상호 작용"을(를) 참조하세요.
필수 조건
이 자습서에서는 Ruby 프로그래밍 언어에 대한 기본적인 이해를 갖추고 있다고 가정합니다.
시작하기 전에 다음 개념을 숙지하는 것이 좋습니다.
GraphQL API와 함께 사용할 수 있는 검사도 있지만 이 자습서에서는 REST API에 중점을 둡니다. GraphQL 개체에 대한 자세한 정보는 GraphQL 설명서의 검사 도구 모음 및 검사 실행을 참조하세요.
설정
다음 섹션에서는 다음 구성 요소를 설정하는 과정을 안내합니다.
- 앱에 대한 코드를 저장할 리포지토리입니다.
- 웹후크를 로컬로 수신하는 방법입니다.
- "검사 도구 모음" 및 "검사 실행" 웹후크 이벤트를 구독하고 있고 검사 대한 쓰기 권한이 있으며, 로컬로 수신할 수 있는 웹후크 URL을 사용하는 GitHub App입니다.
GitHub App에 대한 코드를 저장할 리포지토리 만들기
-
앱에 대한 코드를 저장할 리포지토리를 만듭니다. 자세한 내용은 "새 리포지토리 만들기"을(를) 참조하세요.
-
이전 단계에서 리포지토리를 복제합니다. 자세한 내용은 "리포지토리 복제"을(를) 참조하세요. 로컬 복제본 또는 GitHub Codespaces을(를) 사용할 수 있습니다.
-
터미널에서 복제본이 저장된 디렉터리로 이동합니다.
-
이름이
server.rb
인 Ruby 파일을 만듭니다. 이 파일에는 앱의 모든 코드가 포함됩니다. 나중에 이 파일에 콘텐츠를 추가할 것입니다. -
디렉터리에 파일이 아직
.gitignore
파일이 포함되어 있지 않으면.gitignore
파일을 추가합니다. 나중에 이 파일에 콘텐츠를 추가할 것입니다..gitignore
에 대한 자세한 내용은 "Ignoring files(파일 무시)"을(를) 참조하세요. -
이름이
Gemfile
인 파일을 만듭니다. 이 파일은 Ruby 코드에 필요한 gem 종속성을 설명합니다.Gemfile
에 다음 콘텐츠를 추가합니다.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'
-
이름이
config.ru
인 파일을 만듭니다. 이 파일은 Sinatra 서버가 실행되도록 구성합니다.config.ru
파일에 다음 콘텐츠를 추가합니다.Ruby require './server' run GHAapp
require './server' run GHAapp
웹후크 프록시 URL 가져오기
앱을 로컬에서 개발하려면 웹후크 프록시 URL을 사용하여 웹후크 이벤트를 GitHub에서 컴퓨터 또는 codespace로 전달할 수 있습니다. 이 자습서에서는 Smee.io를 사용하여 웹후크 프록시 URL을 제공하고 이벤트를 전달합니다.
-
터미널에서 다음 명령을 실행하여 Smee 클라이언트를 설치합니다.
Shell npm install --global smee-client
npm install --global smee-client
-
브라우저에서 https://smee.io/(으)로 이동합니다.
-
새 채널 시작을 클릭합니다.
-
"웹후크 프록시 URL" 아래의 전체 URL을 복사합니다.
-
터미널에서 다음 명령을 실행하여 Smee 클라이언트를 시작합니다.
YOUR_DOMAIN
을 이전 단계에서 복사한 웹후크 프록시 URL로 바꿉니다.Shell smee --url YOUR_DOMAIN --path /event_handler --port 3000
smee --url YOUR_DOMAIN --path /event_handler --port 3000
다음과 유사한 출력이 표시됩니다.
Forwarding https://smee.io/YOUR_DOMAIN to http://127.0.0.1:3000/event_handler Connected https://smee.io/YOUR_DOMAIN
smee --url https://smee.io/YOUR_DOMAIN
명령은 Smee 채널에서 받은 모든 웹후크 이벤트를 컴퓨터에서 실행되는 Smee 클라이언트로 전달하도록 Smee에 지시합니다. 이 --path /event_handler
옵션은 이벤트를 /event_handler
경로로 전달합니다. 이 --port 3000
옵션은 포트 3000을 지정합니다. 이 포트는 자습서의 뒷부분에서 코드를 더 추가할 때 서버가 수신 대기하도록 지시할 포트입니다. Smee를 사용하면 컴퓨터가 공용 인터넷에 열려 있지 않아도 GitHub에서 웹후크를 받을 수 있습니다. 브라우저에서 해당 Smee URL을 열어 들어오는 웹후크 페이로드를 검사할 수도 있습니다.
이 가이드의 나머지 단계를 완료하는 동안 이 터미널 창을 열어 두고 Smee를 연결 상태로 유지하는 것이 좋습니다. 고유 도메인을 잃지 않고도 Smee 클라이언트의 연결을 끊고 다시 연결할 수 있지만, 연결된 상태로 두고 다른 터미널 창에서 다른 명령줄 작업을 수행하는 것이 더 쉬울 수 있습니다.
GitHub App 등록
이 자습서에서는 다음과 같은 GitHub App을(를) 등록해야 합니다.
- 웹후크 활성 상태
- 로컬로 수신할 수 있는 웹후크 URL 사용
- "검사" 리포지토리 권한을 갖춤
- "검사 도구 모음" 및 "검사 실행" 웹후크 이벤트를 구독함
다음 단계에서는 이러한 설정으로 GitHub App을(를) 구성하는 방법을 안내합니다. GitHub App 설정에 대한 자세한 내용은 "GitHub 앱 등록"을(를) 참조하세요.
- GitHub Enterprise Server의 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭합니다.
- 계정 설정으로 이동합니다.
- 개인 계정 소유한 앱의 경우 설정을 클릭합니다.
- 조직이 소유한 앱의 경우:
- 사용자의 조직을 클릭합니다.
- 조직 오른쪽에서 설정을 클릭합니다.
- 왼쪽 사이드바에서 개발자 설정을 클릭합니다.
- 왼쪽 사이드바에서 GitHub Apps 을 클릭합니다.
- 새 GitHub 앱을 클릭합니다.
- "GitHub 앱 이름"에서 앱의 이름을 입력합니다. 예를 들어
USERNAME-ci-test-app
에서USERNAME
은 GitHub 사용자 이름입니다. - "홈페이지 URL"에서 앱의 URL을 입력합니다. 예를 들어 생성한 리포지토리의 URL을 사용하여 앱에 대한 코드를 저장할 수 있습니다.
- 이 자습서의 "사용자 식별 및 권한 부여" 및 "설치 후" 섹션은 건너뜁니다.
- "웹후크" 아래에서 활성이 선택되어 있는지 확인합니다.
- "웹후크 URL"에서 이전의 웹후크 프록시 URL을 입력합니다. 자세한 내용은 "웹후크 프록시 URL 가져오기"를 참조하세요.
- "웹후크 비밀"에 임의의 문자열을 입력합니다. 이 비밀은 웹후크가 GitHub에서 전송되는지 확인하는 데 사용됩니다. 이 문자열을 저장하세요. 나중에 사용할 것입니다.
- "리포지토리 권한"의 "검사" 옆에 있는 읽기 및 쓰기를 선택합니다.
- "이벤트 구독"에서 검사 도구 모음 및 검사 실행을 선택합니다.
- "이 GitHub 앱을 설치할 수 있는 위치"에서 이 계정만을 선택합니다. 나중에 앱을 게시하려는 경우 이를 변경할 수 있습니다.
- GitHub 앱 만들기를 클릭합니다.
앱의 식별 정보 및 자격 증명 저장
이 자습서에서는 앱의 자격 증명과 식별 정보를 .env
파일의 환경 변수로 저장하는 방법을 보여 줍니다. 앱을 배포할 때 자격 증명을 저장하는 방법을 변경해야 합니다. 자세한 내용은 "앱 배포"를 참조하세요.
자격 증명을 로컬로 저장하게 되므로 이러한 단계를 수행하기 전에 안전한 컴퓨터를 사용 중인지 확인하세요.
-
터미널에서 복제본이 저장된 디렉터리로 이동합니다.
-
이 디렉터리의 최상위 수준에
.env
라는 파일을 만듭니다. -
.gitignore
파일에.env
를 추가 합니다. 이렇게 하면 실수로 앱의 자격 증명을 커밋하는 것을 방지할 수 있습니다. -
.env
파일에 다음 콘텐츠를 추가합니다.YOUR_HOSTNAME
을 GitHub Enterprise Server 인스턴스의 이름으로 바꿉니다. 이후 단계에서 다른 값을 업데이트합니다.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"
-
앱의 설정 페이지로 이동합니다.
-
GitHub Enterprise Server의 페이지 오른쪽 위 모서리에서 프로필 사진을 클릭합니다.
-
계정 설정으로 이동합니다.
- 개인 계정 소유한 앱의 경우 설정을 클릭합니다.
- 조직이 소유한 앱의 경우:
- 사용자의 조직을 클릭합니다.
- 조직 오른쪽에서 설정을 클릭합니다.
-
왼쪽 사이드바에서 개발자 설정을 클릭합니다.
-
왼쪽 사이드바에서 GitHub Apps 을 클릭합니다.
-
앱 이름 옆에 있는 편집을 클릭합니다.
-
-
앱의 설정 페이지에서 "앱 ID" 옆에 있는 앱의 앱 ID를 찾습니다.
-
.env
파일에서YOUR_APP_ID
를 앱의 앱 ID로 바꿉니다. -
.env
파일에서YOUR_WEBHOOK_SECRET
을 앱의 웹후크 비밀로 바꿉니다. 웹후크 비밀을 잊어버린 경우 "웹후크 비밀(선택 사항)"에서 비밀 변경을 클릭합니다. 새 비밀을 입력한 다음 변경 내용 저장을 클릭합니다. -
앱의 설정 페이지에서 "프라이빗 키"에서 프라이빗 키 생성을 클릭합니다. 컴퓨터에 다운로드된 프라이빗 키
.pem
파일이 나타납니다. -
텍스트 편집기를 사용하여
.pem
파일을 열거나 명령줄에서cat PATH/TO/YOUR/private-key.pem
명령을 사용하여 파일 콘텐츠를 표시합니다. -
파일의 전체 콘텐츠를 복사하여
.env
파일에GITHUB_PRIVATE_KEY
의 값으로 붙여넣고 전체 값 주위에 큰따옴표를 추가합니다.예시 .env 파일은 다음과 같습니다.
GITHUB_APP_IDENTIFIER=12345 GITHUB_WEBHOOK_SECRET=your webhook secret GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END RSA PRIVATE KEY-----"
GitHub App에 대한 코드 추가
이 섹션에서는 GitHub App에 대한 몇 가지 기본 템플릿 코드를 추가하는 방법을 보여주고, 코드가 수행하는 작업을 설명합니다. 자습서의 뒷부분에서는 이 코드를 수정하고 추가하여 앱의 기능을 빌드하는 방법을 알아봅니다.
server.rb
파일에 다음 템플릿 코드를 추가합니다.
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # If a repository name is provided in the webhook, validate that # it consists only of latin alphabetic characters, `-`, and `_`. unless @payload['repository'].nil? halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil? end authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # ADD EVENT HANDLING HERE # 200 # success status end helpers do # ADD CREATE_CHECK_RUN HELPER METHOD HERE # # ADD INITIATE_CHECK_RUN HELPER METHOD HERE # # ADD CLONE_REPOSITORY HELPER METHOD HERE # # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) # request.body is an IO or StringIO object # Rewind in case someone already read it request.body.rewind # The raw text of the body is required for webhook signature verification @payload_raw = request.body.read begin @payload = JSON.parse @payload_raw rescue => e fail 'Invalid JSON (#{e}): #{@payload_raw}' end end # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app and not altered by # a malicious third party. def authenticate_app payload = { # The time that this JWT was issued, _i.e._ now. iat: Time.now.to_i, # JWT expiration time (10 minute maximum) exp: Time.now.to_i + (10 * 60), # Your GitHub App's identifier number iss: APP_IDENTIFIER } # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) @installation_id = payload['installation']['id'] @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to # create the hash signature sent in the `X-HUB-Signature` header of each # webhook. This code computes the expected hash signature and compares it to # the signature sent in the `X-HUB-Signature` header. If they don't match, # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw) halt 401 unless their_digest == our_digest # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end # Finally some logic to let us run this server directly from the command line, # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the # Sinatra run method run! if __FILE__ == $0 end
require 'sinatra/base' # Use the Sinatra web framework
require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load' # Manages environment variables
require 'json' # Allows your app to manipulate JSON data
require 'openssl' # Verifies the webhook signature
require 'jwt' # Authenticates a GitHub App
require 'time' # Gets ISO 8601 representation of a Time object
require 'logger' # Logs debug statements
# This code is a Sinatra app, for two reasons:
# 1. Because the app will require a landing page for installation.
# 2. To easily handle webhook events.
class GHAapp < Sinatra::Application
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# If a repository name is provided in the webhook, validate that
# it consists only of latin alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
helpers do
# ADD CREATE_CHECK_RUN HELPER METHOD HERE #
# ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
# ADD CLONE_REPOSITORY HELPER METHOD HERE #
# ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
# Saves the raw payload and converts the payload to JSON format
def get_payload_request(request)
# request.body is an IO or StringIO object
# Rewind in case someone already read it
request.body.rewind
# The raw text of the body is required for webhook signature verification
@payload_raw = request.body.read
begin
@payload = JSON.parse @payload_raw
rescue => e
fail 'Invalid JSON (#{e}): #{@payload_raw}'
end
end
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app and not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT.
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
# Instantiate an Octokit client, authenticated as an installation of a
# GitHub App, to run API operations.
def authenticate_installation(payload)
@installation_id = payload['installation']['id']
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
end
# Check X-Hub-Signature to confirm that this webhook was generated by
# GitHub, and not a malicious third party.
#
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
# create the hash signature sent in the `X-HUB-Signature` header of each
# webhook. This code computes the expected hash signature and compares it to
# the signature sent in the `X-HUB-Signature` header. If they don't match,
# this request is an attack, and you should reject it. GitHub uses the HMAC
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
# like this: 'sha1=123456'.
def verify_webhook_signature
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
halt 401 unless their_digest == our_digest
# The X-GITHUB-EVENT header provides the name of the event.
# The action value indicates the which action triggered the event.
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
end
end
# Finally some logic to let us run this server directly from the command line,
# or with Rack. Don't worry too much about this code. But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the
# Sinatra run method
run! if __FILE__ == $0
end
이 섹션의 나머지 부분에서는 템플릿 코드가 수행하는 작업을 설명합니다. 이 섹션에서 완료해야 하는 단계는 없습니다. 템플릿 코드에 이미 익숙한 경우 "서버 시작"으로 건너뛸 수 있습니다.
템플릿 코드 이해
텍스트 편집기에서 server.rb
파일을 엽니다. 템플릿 코드에 대한 추가 컨텍스트를 제공하는 주석이 파일 전체에 표시됩니다. 주석을 주의 깊게 읽고, 새 코드를 작성하면서 직접 메모를 추가하는 것이 좋습니다.
필수 파일 목록 아래에 표시되는 첫 번째 코드는 class GHApp < Sinatra::Application
선언입니다. 이 클래스 내에서 GitHub App에 대한 모든 코드를 작성합니다. 다음 섹션에서는 이 클래스 내에서 코드가 수행하는 작업을 자세히 설명합니다.
포트 설정
class GHApp < Sinatra::Application
선언 내에서 가장 먼저 볼 수 있는 set :port 3000
입니다. 웹 서버를 시작할 때 사용되는 포트를 "웹후크 프록시 URL 가져오기"에서 웹후크 페이로드를 리디렉션한 포트와 일치하도록 설정합니다.
# 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']
로깅 켜기
다음은 개발 중에 로깅을 사용하도록 설정하는 코드 블록이며, 이는 Sinatra의 기본 환경입니다. 이 코드는 앱을 개발하는 동안 터미널에 유용한 출력을 표시하도록 DEBUG
수준에서 로깅을 사용합니다.
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
before
필터 정의
Sinatra는 경로 처리기 전에 코드를 실행할 수 있는 before
필터를 사용합니다. 템플릿의 before
블록은 4개의 도우미 메서드(get_payload_request
, verify_webhook_signature
, authenticate_app
, authenticate_installation
)를 호출합니다. 자세한 내용은 Sinatra 설명서의 "필터" 및 "도우미"를 참조하세요.
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# If a repository name is provided in the webhook, validate that
# it consists only of latin alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
이러한 각 도우미 메서드는 코드의 뒷부분에서 helpers do
로 시작하는 코드 블록에서 정의됩니다. 자세한 내용은 "도우미 메서드 정의"를 참조하세요.
verify_webhook_signature
에서는 unless @payload
(으)로 시작하는 코드가 보안 조치입니다. 리포지토리 이름에 웹후크 페이로드가 제공되면 이 코드는 리포지토리 이름에 라틴어 알파벳 문자, 하이픈 및 밑줄만 포함되어 있는지 유효성을 검사합니다. 이렇게 하면 불량 행위자가 임의 명령을 실행하거나 거짓 리포지토리 이름을 삽입하지 않도록 할 수 있습니다. 나중에 helpers do
(으)로 시작하는 코드 블록에서 verify_webhook_signature
도우미 메서드는 또한 추가 보안 조치로서 수신되는 웹후크 페이로드의 유효성을 검사합니다.
경로 처리기 정의
빈 경로가 템플릿 코드에 포함됩니다. 이 코드는 /event_handler
경로에 대한 모든 POST
요청을 처리합니다. 나중에 여기에 더 많은 코드를 추가합니다.
post '/event_handler' do
end
도우미 메서드 정의
템플릿의 before
블록에는 4개의 도우미 메서드가 호출됩니다. helpers do
코드 블록은 이러한 각 도우미 메서드를 정의합니다.
웹후크 페이로드 처리
첫 번째 도우미 메서드 get_payload_request
은(는) 웹후크 페이로드를 캡처하고 JSON 형식으로 변환하므로 페이로드의 데이터에 훨씬 쉽게 액세스할 수 있습니다.
웹후크 서명 확인
두 번째 도우미 메서드 verify_webhook_signature
은(는) 웹후크 서명을 확인하여 GitHub 이벤트를 생성했는지 확인합니다. verify_webhook_signature
도우미 메서드의 코드에 대한 자세한 내용은 "웹후크 제공 유효성 검사하기"을(를) 참조하세요. 웹후크가 안전한 경우 이 메서드는 들어오는 모든 페이로드를 터미널에 기록합니다. 로거 코드는 웹 서버가 작동 중인지 확인하는 데 유용합니다.
GitHub App으로 인증
세 번째 도우미 메서드 authenticate_app
을(를) 사용하면 GitHub App에서 인증할 수 있으므로 설치 토큰을 요청할 수 있습니다.
API를 호출하려면 Octokit 라이브러리를 사용합니다. 이 라이브러리를 사용하여 흥미로운 작업을 수행하려면 GitHub App 인증을 받아야 합니다. Octokit 라이브러리에 대한 자세한 내용은 Octokit 설명서를 참조하세요.
GitHub Apps에는 세 가지 인증 메서드가 있습니다.
- JSON Web Token(JWT)을 사용하여 GitHub App(으)로 인증합니다.
- 설치 액세스 토큰을 사용하여 GitHub App의 특정 설치로 인증합니다.
- 사용자를 대신하여 인증합니다. 이 자습서에서는 이 인증 방법을 사용하지 않습니다.
다음 섹션 "설치로 인증"에서 설치로 인증하는 방법에 대해 알아봅니다.
GitHub App으로 인증하면 다음과 같은 몇 가지 작업을 수행할 수 있습니다.
- GitHub App에 대한 높은 수준의 관리 정보를 검색할 수 있습니다.
- 앱 설치에 대한 액세스 토큰을 요청할 수 있습니다.
예를 들어 GitHub App(으)로 인증하여 앱을 설치한 계정(조직 및 개인)의 목록을 검색합니다. 그러나 이 인증 방법을 사용하면 API로 많은 작업을 수행할 수 없습니다. 설치를 대신하여 리포지토리의 데이터에 액세스하고 작업을 수행하려면 설치로 인증해야 합니다. 이렇게 하려면 먼저 GitHub App(으)로 인증하여 설치 액세스 토큰을 요청해야 합니다. 자세한 내용은 "GitHub 앱을 사용한 인증 정보"을(를) 참조하세요.
Octokit.rb 라이브러리를 사용하여 API를 호출하려면 먼저 authenticate_app
도우미 메서드를 사용하여 GitHub App(으)로 인증된 Octokit 클라이언트를 초기화해야 합니다.
# 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
위 코드는 JWT(JSON 웹 토큰)를 생성하고 이를 앱의 프라이빗 키와 함께 사용하여 Octokit 클라이언트를 초기화합니다. GitHub 앱의 저장된 퍼블릭 키를 사용하여 토큰을 확인해 요청의 인증을 확인합니다. 해당 코드의 작동 방식에 대한 자세한 내용은 "GitHub 앱에 대한 JWT(JSON Web Token) 생성"을(를) 참조하세요.
설치로 인증
네 번째이자 마지막 도우미 메서드 authenticate_installation
은(는) 설치로 인증된 Octokit 클라이언트를 초기화하며, 이를 통해 API에 인증된 호출을 만드는 데 사용할 수 있습니다.
_설치_는 앱을 설치한 사용자 또는 조직 계정을 나타냅니다. 누군가가 해당 계정의 둘 이상의 리포지토리에 대한 앱 액세스 권한을 부여하더라도 동일한 계정 내에 있기 때문에 하나의 설치로만 집계됩니다.
# 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
create_app_installation_access_token
Octokit 메서드는 설치 토큰을 만듭니다. 자세한 내용은 Octokit 설명서의 "create_installation_access_token"을 참조하세요.
해당 메서드는 두 개의 인수를 허용합니다.
- 설치(정수): GitHub App 설치의 ID
- 옵션(해시, 기본값:
{}
): 사용자 지정 가능한 옵션의 집합
GitHub App이(가) 웹후크를 받으면 앱에 id
가 있는 installation
이 포함됩니다. GitHub App(으)로 인증된 클라이언트를 사용하여 이 ID를 create_app_installation_access_token
메서드에 전달하여 각 설치에 대한 액세스 토큰을 생성합니다. 메서드에 옵션을 전달하지 않으므로 옵션은 기본적으로 빈 해시로 설정됩니다. create_app_installation_access_token
에 대한 응답에는 token
과 expired_at
이라는 두 개의 필드가 포함됩니다. 템플릿 코드는 응답에서 토큰을 선택하고 설치 클라이언트를 초기화합니다.
이 메서드를 사용하면 앱이 새 웹후크 페이로드를 받을 때마다 이벤트를 트리거한 설치에 대한 클라이언트를 만듭니다. 이 인증 프로세스를 사용하면 GitHub App이(가) 모든 계정의 모든 설치에서 작동할 수 있습니다.
서버 시작하기
앱은 아직 아무 작업도 수행하지 않지만 이 시점에서 서버에서 앱을 실행할 수 있습니다.
-
터미널에서 Smee가 아직 실행 중인지 확인합니다. 자세한 내용은 "웹후크 프록시 URL 가져오기"를 참조하세요.
-
터미널에서 새 탭을 열고 자습서의 앞부분에서 만든 리포지토리를 복제한 디렉터리로
cd
합니다. 자세한 내용은 "GitHub 앱에 대한 코드를 저장할 리포지토리 만들기"를 참조하세요. 이 리포지토리의 Ruby 코드는 Sinatra 웹 서버를 시작합니다. -
다음 두 명령을 차례로 실행하여 종속성을 설치합니다.
Shell gem install bundler
gem install bundler
Shell bundle install
bundle install
-
종속성을 설치한 후 다음 명령을 실행하여 서버를 시작합니다.
Shell bundle exec ruby server.rb
bundle exec ruby server.rb
다음과 같은 응답이 표시됩니다.
> == 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
오류가 표시되면
server.rb
가 포함된 디렉터리에.env
파일을 만들었는지 확인합니다. -
서버를 테스트하려면 브라우저에서
http://localhost:3000
으로 이동합니다."Sinatra가 이 ditty를 모릅니다"라는 오류 페이지가 표시되면 앱이 예상대로 작동하고 있는 것입니다. 오류 페이지임에도 불구하고 Sinatra 오류 페이지입니다. 즉, 앱이 예상대로 서버에 연결되어 있습니다. 앱에 표시할 다른 내용이 없으므로 이 메시지가 표시됩니다.
서버가 앱을 수신 대기하고 있는지 테스트합니다.
서버가 수신할 이벤트를 트리거하여 앱을 수신 대기하고 있는지 테스트할 수 있습니다. 테스트 리포지토리에 앱을 설치하면 installation
이벤트가 앱으로 전송됩니다. 앱이 이를 수신하는 경우 server.rb
를 실행 중인 터미널 탭에 출력이 표시됩니다.
-
자습서 코드를 테스트하는 데 사용할 새 리포지토리를 만듭니다. 자세한 내용은 "새 리포지토리 만들기"을(를) 참조하세요.
-
방금 만든 리포지토리에 GitHub App을(를) 설치합니다. 자세한 내용은 "자신만의 GitHub 앱 설치"을(를) 참조하세요. 설치 프로세스 중에 리포지토리만 선택을 선택하고 이전 단계에서 만든 리포지토리를 선택합니다.
-
설치를 클릭한 후
server.rb
를 실행 중인 터미널 탭의 출력을 확인합니다. 다음과 비슷한 결과가 표시됩니다.> 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
이와 같은 출력이 표시되면 앱이 GitHub 계정에 설치되었다는 알림을 받았다는 의미입니다. 앱이 예상대로 서버에서 실행되고 있습니다.
이 출력이 표시되지 않으면 다른 터미널 탭에서 Smee가 올바르게 실행되고 있는지 확인합니다. Smee를 다시 시작해야 하는 경우 앱을 _제거_하고 _다시 설치_하여
installation
이벤트를 앱에 다시 보낸 후 터미널에서 출력을 확인해야 합니다.
위의 터미널 출력이 어디에서 나오는지 궁금하다면 "GitHub App에 대한 코드 추가"에서 server.rb
에 추가한 앱 템플릿 코드에 기록되어 있습니다.
1부. Checks API 인터페이스 만들기
1부에서는 check_suite
웹후크 이벤트를 수신하고 검사 실행을 만들고 업데이트하는 데 필요한 코드를 추가합니다. GitHub에서 검사가 다시 요청되었을 때 검사 실행을 만드는 방법도 알아봅니다. 이 섹션의 마지막에서는 GitHub 끌어오기 요청에서 만든 검사 실행을 확인할 수 있습니다.
검사 실행은 이 섹션의 코드에 대한 검사를 수행하지 않습니다. "2부: CI 테스트 만들기"에서 해당 기능을 추가합니다.
웹후크 페이로드를 로컬 서버로 전달하는 Smee 채널이 이미 구성되어 있어야 합니다. 서버가 실행 중이고 테스트 리포지토리에 등록하고 설치한 GitHub App에 연결되어야 합니다.
1부에서 완료할 단계는 다음과 같습니다.
1.1단계. 이벤트 처리 추가
앱이 검사 도구 모음 및 검사 실행 이벤트를 구독했으므로 check_suite
및 check_run
웹후크를 수신합니다. GitHub은(는) 웹후크 페이로드를 POST
요청으로 전송합니다. Smee 웹후크 페이로드를 http://localhost:3000/event_handler
로 전달했으므로 서버는 post '/event_handler'
경로에서 POST
요청 페이로드를 받게 됩니다.
"GitHub App에 대한 코드 추가"에서 만든 server.rb
파일을 열고 다음 코드를 찾습니다. 템플릿 코드에는 빈 post '/event_handler'
경로가 이미 포함되어 있습니다. 빈 경로는 다음과 같습니다.
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
post '/event_handler' do
로 시작하는 코드 블록(# ADD EVENT HANDLING HERE #
라고 표시됨)에서 다음 코드를 추가합니다. 이 경로는 check_suite
이벤트를 처리합니다.
# Get the event type from the HTTP_X_GITHUB_EVENT header case request.env['HTTP_X_GITHUB_EVENT'] when 'check_suite' # A new check_suite has been created. Create a new check run with status queued if @payload['action'] == 'requested' || @payload['action'] == 'rerequested' create_check_run end # ADD CHECK_RUN METHOD HERE # end
# Get the event type from the HTTP_X_GITHUB_EVENT header
case request.env['HTTP_X_GITHUB_EVENT']
when 'check_suite'
# A new check_suite has been created. Create a new check run with status queued
if @payload['action'] == 'requested' || @payload['action'] == 'rerequested'
create_check_run
end
# ADD CHECK_RUN METHOD HERE #
end
GitHub에서 보내는 모든 이벤트에는 POST
요청의 이벤트 유형을 나타내는 HTTP_X_GITHUB_EVENT
라는 요청 헤더가 포함됩니다. 지금은 새 검사 도구 모음을 만들 때 내보내지는 check_suite
형식의 이벤트에만 관심이 있습니다. 각 이벤트에는 이벤트를 트리거한 작업 유형을 나타내는 추가 action
필드가 있습니다. check_suite
의 경우 action
필드는 requested
, rerequested
또는 completed
가 될 수 있습니다.
requested
작업은 코드가 리포지토리에 푸시될 때마다 검사 실행을 요청하고, rerequested
작업은 리포지토리에 이미 있는 코드에 대한 검사를 다시 실행하도록 요청합니다. requested
및 rerequested
작업에서 모두 검사 실행 만들기를 요구하므로 create_check_run
이라는 도우미를 호출합니다. 이제 해당 메서드를 작성해 보겠습니다.
1.2 단계. 확인 실행 만들기
다른 경로로도 이 새 메서드를 사용하려면 메서드를 Sinatra 도우미로 추가합니다.
helpers do
로 시작하는 코드 블록(# ADD CREATE_CHECK_RUN HELPER METHOD HERE #
라고 표시됨)에서 다음 코드를 추가합니다.
# 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
이 코드는 Octokit create_check_run 메서드를 사용하여 POST /repos/{owner}/{repo}/check-runs
엔드포인트를 호출합니다. 이 엔드포인트에 대한 자세한 내용은 "검사 실행에 대한 REST API 엔드포인트" 항목을 참조하세요.
검사 실행을 만들려면 name
과 head_sha
라는 두 개의 입력 매개 변수만 필요합니다. 이 코드에서는 자습서의 뒷부분에서 RuboCop을 사용하여 CI 테스트를 구현하기 때문에 검사 실행 이름을 "Octo RuboCop"으로 지정합니다. 그러나 검사 실행에 원하는 이름을 선택할 수 있습니다. RuboCop에 대한 자세한 내용은 RuboCop 설명서를 참조하세요.
지금은 기본 기능이 작동하도록 필수 매개 변수만 제공하지만 나중에 검사 실행에 대한 자세한 정보를 수집할 때는 검사 실행을 업데이트합니다. 기본적으로 GitHub은(는) status
를 queued
로 설정합니다.
GitHub에서는 특정 커밋 SHA에 대한 검사 실행을 만듭니다. 따라서 head_sha
이(가) 필수 매개 변수입니다. 웹후크 페이로드에서 커밋 SHA를 찾을 수 있습니다. 지금은 check_suite
이벤트에 대한 검사 실행만 만들고 있지만 head_sha
가 이벤트 페이로드의 check_suite
및 check_run
개체 모두에 포함된다는 것을 아는 것이 좋습니다.
위의 코드에서는 if/else
문처럼 작동하는 삼항 연산자를 사용하여 페이로드에 check_run
개체가 포함되어 있는지 확인합니다. 포함되어 있는 경우 check_run
개체에서 head_sha
를 읽고, 포함되어 있지 않은 경우 check_suite
개체에서 읽습니다.
코드 테스트
다음 단계에서는 코드가 작동하는지 테스트하고 새 검사 실행을 성공적으로 만드는 방법을 보여 줍니다.
-
다음 명령을 실행하여 터미널에서 서버를 다시 시작합니다. 서버가 이미 실행 중인 경우 먼저 터미널에
Ctrl-C
를 입력하여 서버를 중지한 후 다음 명령을 실행하여 서버를 다시 시작합니다.Shell ruby server.rb
ruby server.rb
-
"서버가 앱을 수신 대기하고 있는지 테스트"에서 만든 테스트 리포지토리에서 끌어오기 요청을 만듭니다. 이는 앱 액세스 권한을 부여한 리포지토리입니다.
-
방금 만든 끌어오기 요청에서 검사 탭으로 이동합니다. "Octo RuboCop"이라는 이름 또는 이전에 검사 실행에 대해 선택한 이름의 검사 실행이 표시됩니다.
검사 탭에 다른 앱이 표시되는 경우 검사에 대한 읽기 및 쓰기 권한이 있고 검사 도구 모음 및 검사 실행 이벤트를 구독하는 다른 앱이 리포지토리에 설치되어 있음을 의미합니다. 또한 리포지토리에 pull_request
또는 pull_request_target
이벤트에 의해 트리거된 GitHub Actions 워크플로가 있음을 의미할 수도 있습니다.
지금까지 GitHub에게 검사 실행을 만들라고 지시했습니다. 끌어오기 요청의 검사 실행 상태가 노란색 아이콘으로 큐에 대기로 설정됩니다. 다음 단계에서는 GitHub이(가) 검사 실행을 만들고 해당 상태를 업데이트할 때까지 기다립니다.
1.3단계. 확인 실행 업데이트
create_check_run
메서드가 실행되면 GitHub에게 새 검사 실행을 만들도록 요청합니다. GitHub(이)가 검사 실행 만들기를 마치면 created
작업과 함께 check_run
웹후크 이벤트를 받게 됩니다. 이 이벤트는 검사 실행을 시작하는 신호입니다.
created
작업을 찾으려면 이벤트 처리기를 업데이트해야 합니다. 이벤트 처리기를 업데이트하는 동안 rerequested
작업에 대한 조건을 추가할 수 있습니다. 누군가가 "다시 실행" 단추를 클릭하여 GitHub에서 단일 테스트를 다시 실행하면 GitHub이(가) 앱에 rerequested
검사 실행 이벤트를 보냅니다. 검사 실행이 rerequested
인 경우에는 프로세스를 처음부터 시작하고 새 검사 실행을 만드는 것이 좋습니다. 이렇게 하려면 post '/event_handler'
경로에 check_run
이벤트에 대한 조건을 포함합니다.
post '/event_handler' do
로 시작하는 코드 블록(# ADD CHECK_RUN METHOD HERE #
라고 표시됨)에서 다음 코드를 추가합니다.
when 'check_run' # Check that the event is being sent to this app if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER case @payload['action'] when 'created' initiate_check_run when 'rerequested' create_check_run # ADD REQUESTED_ACTION METHOD HERE # end end
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
# ADD REQUESTED_ACTION METHOD HERE #
end
end
GitHub에서 필요한 검사 권한이 있는 리포지토리에 설치된 모든 앱에 created
검사 실행에 대한 모든 이벤트를 보냅니다. 즉, 앱이 다른 앱에서 만든 검사 실행을 받게 됩니다. created
검사 실행은 검사 실행을 요청받고 있는 앱으로만 GitHub에서 보내는 requested
또는 rerequested
검사 도구 모음과 약간 다릅니다. 위의 코드는 검사 실행의 애플리케이션 ID를 찾습니다. 이렇게 하면 리포지토리의 다른 앱에 대한 모든 검사 실행이 필터링됩니다.
다음으로 검사 실행 상태를 업데이트하고 CI 테스트를 시작할 준비를 하는 initiate_check_run
메서드를 작성합니다.
이 섹션에서는 CI 테스트를 아직 시작하지는 않지만 검사 실행의 상태를 queued
에서 pending
으로 업데이트한 다음 pending
에서 completed
로 업데이트하여 검사 실행의 전체 흐름을 확인하는 방법을 안내합니다. “2부: CI 테스트 만들기”에서 실제로 CI 테스트를 수행하는 코드를 추가합니다.
initiate_check_run
메서드를 만들고 검사 실행의 상태를 업데이트해 보겠습니다.
helpers do
로 시작하는 코드 블록(# ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
라고 표시됨)에서 다음 코드를 추가합니다.
# 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
위의 코드는 update_check_run
Octokit 메서드를 사용하여 PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
엔드포인트를 호출하고 이미 생성한 검사 실행을 업데이트합니다. 이 엔드포인트에 대한 자세한 내용은 "검사 실행에 대한 REST API 엔드포인트" 항목을 참조하세요.
이 코드가 수행하는 작업을 살펴보겠습니다. 먼저 검사 실행 상태를 in_progress
로 업데이트하고 started_at
시간을 현재 시간으로 암시적으로 설정합니다. 자습서의 2부에서는 ***** RUN A CI TEST *****
아래에 실제 CI 테스트를 시작하는 코드를 추가합니다. 지금은 해당 섹션을 자리 표시자로 남겨 두므로 뒤에 오는 코드는 CI 프로세스가 성공하고 모든 테스트가 통과한다는 것을 시뮬레이션합니다. 마지막으로 코드는 검사 실행의 상태를 다시 completed
로 업데이트합니다.
REST API를 사용하여 completed
의 검사 실행 상태를 제공하는 경우 conclusion
및 completed_at
매개 변수가 필요합니다. conclusion
에 검사 실행의 결과가 요약되어 있으며 결과는 success
, failure
, neutral
, cancelled
, timed_out
, skipped
또는 action_required
가 될 수 있습니다. 결론을 success
로, completed_at
시간을 현재 시간으로, 상태를 completed
로 설정합니다.
검사에 대한 자세한 내용을 제공할 수도 있지만 다음 섹션에서 제공하도록 합니다.
코드 테스트
다음 단계에서는 코드가 작동하는지 테스트하는 방법과 새로 만든 "모두 다시 실행" 단추가 작동하는지를 보여 줍니다.
-
다음 명령을 실행하여 터미널에서 서버를 다시 시작합니다. 서버가 이미 실행 중인 경우 먼저 터미널에
Ctrl-C
를 입력하여 서버를 중지한 후 다음 명령을 실행하여 서버를 다시 시작합니다.Shell ruby server.rb
ruby server.rb
-
"서버가 앱을 수신 대기하고 있는지 테스트"에서 만든 테스트 리포지토리에서 끌어오기 요청을 만듭니다. 이는 앱 액세스 권한을 부여한 리포지토리입니다.
-
방금 만든 끌어오기 요청에서 검사 탭으로 이동합니다. "모두 다시 실행" 단추가 표시됩니다.
-
오른쪽 위에 있는 "모두 다시 실행" 단추를 클릭합니다. 테스트가 다시 실행되고
success
로 끝납니다.
2부. CI 테스트 만들기
이제 API 이벤트를 수신하고 검사 실행을 만들기 위해 인터페이스를 만들었으므로 CI 테스트를 구현하는 검사 실행을 만들 수 있습니다.
RuboCop은 Ruby 코드 Linter 및 포맷터로, Ruby 코드를 검사하여 Ruby 스타일 가이드를 준수하는지 확인합니다. 자세한 내용은 RuboCop 설명서를 참조하세요.
RuboCop에는 다음과 같은 세 가지 기본 기능이 있습니다.
- 코드 스타일을 확인하는 린팅
- 코드 서식
ruby -w
를 사용하여 네이티브 Ruby 린팅 기능을 대체합니다.
앱은 CI 서버에서 RuboCop을 실행하고 RuboCop이 GitHub에 보고하는 결과를 보고하는 검사 실행(이 경우 CI 테스트)을 만듭니다.
REST API를 사용하면 상태, 이미지, 요약, 주석, 요청된 작업을 포함하여 각 검사 실행에 대한 다양한 세부 정보를 보고할 수 있습니다.
주석은 리포지토리의 특정 코드 줄에 대한 정보입니다. 주석을 사용하면 추가 정보를 표시하려는 코드의 정확한 부분을 정확히 파악하고 시각화할 수 있습니다. 예를 들어 특정 코드 줄에 해당 정보를 주석, 오류 또는 경고로 표시할 수 있습니다. 이 자습서에서는 주석을 사용하여 RuboCop 오류를 시각화합니다.
요청된 작업을 활용하기 위해 앱 개발자는 끌어오기 요청의 검사 탭에서 단추를 만들 수 있습니다. 누군가가 해당 단추 중 하나를 클릭하면 GitHub App에 requested_action``check_run
이벤트가 전송됩니다. 앱이 수행하는 작업은 앱 개발자가 전부 구성할 수 있습니다. 이 자습서에서는 사용자가 RuboCop에서 발견한 오류를 수정하도록 요청할 수 있는 단추를 추가하는 방법을 안내합니다. RuboCop은 명령줄 옵션을 사용하여 오류 자동 수정을 지원하며 사용자는 이 옵션을 활용하도록 requested_action
을 구성합니다.
이 섹션에서 완료할 단계는 다음과 같습니다.
- Ruby 파일 추가
- RuboCop이 테스트 리포지토리를 복제하도록 허용
- RuboCop 실행
- RuboCop 오류 수집
- CI 테스트 결과를 사용하여 검사 실행 업데이트
- RuboCop 오류 자동 수정
2.1 단계. Ruby 파일 추가
RuboCop에 대한 특정 파일 또는 전체 디렉터리를 전달하여 확인할 수 있습니다. 이 자습서에서는 전체 디렉터리에서 RuboCop을 실행합니다. RuboCop은 Ruby 코드만 검사합니다. GitHub App을(를) 테스트하려면 RuboCop이 찾을 수 있는 오류가 포함된 Ruby 파일을 리포지토리에 추가해야 합니다. 다음 Ruby 파일을 리포지토리에 추가한 후 코드에서 RuboCop을 실행하도록 CI 검사 업데이트합니다.
-
"서버가 앱을 수신 대기하고 있는지 테스트"에서 만든 테스트 리포지토리로 이동합니다. 이는 앱 액세스 권한을 부여한 리포지토리입니다.
-
이름이
myfile.rb
인 새 파일을 만듭니다. 자세한 내용은 "새 파일 만들기"을(를) 참조하세요. -
myfile.rb
에 다음 콘텐츠를 추가합니다.Ruby # frozen_string_literal: true # The Octocat class tells you about different breeds of Octocat class Octocat def initialize(name, *breeds) # Instance variables @name = name @breeds = breeds end def display breed = @breeds.join("-") puts "I am of #{breed} breed, and my name is #{@name}." end end m = Octocat.new("Mona", "cat", "octopus") m.display
# frozen_string_literal: true # The Octocat class tells you about different breeds of Octocat class Octocat def initialize(name, *breeds) # Instance variables @name = name @breeds = breeds end def display breed = @breeds.join("-") puts "I am of #{breed} breed, and my name is #{@name}." end end m = Octocat.new("Mona", "cat", "octopus") m.display
-
파일을 로컬로 만든 경우 파일을 GitHub의 리포지토리로 커밋하고 푸시해야 합니다.
2.2 단계. RuboCop이 테스트 리포지토리를 복제하도록 허용
RuboCop은 명령줄 유틸리티로 사용할 수 있습니다. 즉, 리포지토리에서 RuboCop을 실행하려면 GitHub App이(가) CI 서버에서 리포지토리의 로컬 복사본을 복제해야 RuboCop에서 파일을 구문 분석할 수 있습니다. 이렇게 하려면 코드에서 Git 작업을 실행할 수 있어야 하며 GitHub App에는 리포지토리를 복제하기 위한 올바른 권한이 있어야 합니다.
Git 작업 허용
Ruby 앱에서 Git 작업을 실행하려면 ruby-git gem을 사용할 수 있습니다. "설정"에서 만든 Gemfile
에는 ruby-git gem이 이미 포함되어 있으며 "서버 시작"에서 bundle install
을 실행했을 때 설치되었습니다.
이제 server.rb
파일 상단의 다른 require
항목 아래에 다음 코드를 추가합니다.
require 'git'
require 'git'
앱 권한 업데이트
다음으로 GitHub App의 권한을 업데이트해야 합니다. 리포지토리를 복제하려면 앱에 "콘텐츠"에 대한 읽기 권한이 필요합니다. 이 자습서의 뒷부분에서는 콘텐츠를 GitHub에 푸시할 수 있는 쓰기 권한이 필요합니다. 앱의 사용 권한 업데이트 방법은 다음과 같습니다.
- 앱 설정에서 앱을 선택하고 사이드바에서 사용 권한 및 이벤트를 클릭합니다.
- "리포지토리 권한"의 "콘텐츠" 옆에 있는 읽기 및 쓰기를 선택합니다.
- 페이지 아래쪽에 있는 변경 내용 저장을 클릭합니다.
- 계정에 앱을 설치한 경우 메일을 확인하고 링크를 따라 새 사용 권한을 수락합니다. 앱의 사용 권한 또는 웹후크를 변경할 때마다 앱을 설치한 사용자(자신 포함)는 변경 내용이 적용되기 전에 새 권한을 수락해야 합니다. 설치 페이지로 이동하여 새 권한을 수락할 수도 있습니다. 앱 이름 아래에 앱이 다른 권한을 요청하고 있음을 알리는 링크가 표시됩니다. 요청 검토를 클릭한 다음 새 권한 수락을 클릭합니다.
리포지토리를 복제하는 코드 추가
리포지토리를 복제하려면 코드는 GitHub App의 권한 및 Octokit SDK를 사용하여 앱(x-access-token:TOKEN
)에 대한 설치 토큰을 만들고 다음 복제 명령에 사용합니다.
git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git
위 명령은 HTTP를 통해 리포지토리를 복제합니다. 리포지토리 소유자(사용자 또는 조직) 및 리포지토리 이름을 포함하는 전체 리포지토리 이름이 필요합니다. 예를 들어 octocat Hello-World 리포지토리의 전체 이름은 octocat/hello-world
입니다.
server.rb
파일을 엽니다. helpers do
로 시작하는 코드 블록(# ADD CLONE_REPOSITORY HELPER METHOD HERE #
라고 표시됨)에서 다음 코드를 추가합니다.
# 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
위의 코드는 ruby-git
gem을 사용하여 앱의 설치 토큰을 통해 리포지토리를 복제합니다. 이는 server.rb
와 동일한 디렉터리에 있는 코드를 복제합니다. 리포지토리에서 Git 명령을 실행하려면 코드를 리포지토리 디렉터리로 변경해야 합니다. 디렉터리를 변경하기 전에 코드는 현재 작업 디렉터리를 변수(pwd
)에 저장하여 clone_repository
메서드를 끝내기 전에 반환할 위치를 기억합니다.
이 코드는 리포지토리 디렉터리에서 최신 변경 내용을 가져와 병합하고(@git.pull
) 특정 Git 참조를 체크 아웃합니다(@git.checkout(ref)
). 이 모든 작업을 수행하는 코드는 자체 메서드에 잘 맞습니다. 작업을 수행하려면 메서드에 체크 아웃할 리포지토리와 참조의 이름과 전체 이름이 필요합니다. 참조는 커밋 SHA, 분기 또는 태그가 될 수 있습니다. 완료되면 코드는 디렉터리를 원래 작업 디렉터리(pwd
)로 다시 변경합니다.
이제 리포지토리를 복제하고 참조를 체크 아웃하는 메서드가 있습니다. 다음으로, 필요한 입력 매개 변수를 가져와서 새 clone_repository
메서드를 호출하는 코드를 추가해야 합니다.
helpers do
로 시작하는 코드 블록(# ***** RUN A CI TEST *****
라고 표시됨)에서 initiate_check_run
도우미 메서드에 다음 코드를 추가합니다.
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 #
위의 코드는 check_run
웹후크 페이로드에서 전체 리포지토리 이름 및 커밋의 헤드 SHA를 가져옵니다.
2.3 단계. RuboCop 실행
지금까지 코드는 리포지토리를 복제하고 CI 서버를 사용하여 검사 실행을 만들었습니다. 이제 RuboCop Linter 및 검사 주석의 세부 정보를 살펴보겠습니다.
먼저 RuboCop을 실행하는 코드를 추가하고 스타일 코드 오류를 JSON 형식으로 저장합니다.
helpers do
로 시작하는 코드 블록에서 initiate_check_run
도우미 메서드를 찾습니다. 해당 도우미 메서드 내부의 clone_repository(full_repo_name, repository, head_sha)
(# ADD CODE HERE TO RUN RUBOCOP #
(이)라고 표시됨) 아래에 다음 코드를 추가합니다.
# 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 #
위의 코드는 리포지토리의 디렉터리에 있는 모든 파일에서 RuboCop을 실행합니다. --format json
옵션은 린팅 결과의 복사본을 컴퓨터 구문 분석 가능한 형식으로 저장합니다. 자세한 내용 및 JSON 형식의 예시는 RuboCop docs의 "JSON 포맷터"를 참조하세요. 또한 이 코드는 JSON을 구문 분석하므로 @output
변수를 사용하여 GitHub App의 키와 값에 쉽게 액세스할 수 있습니다.
RuboCop을 실행하고 린팅 결과를 저장한 후 이 코드는 rm -rf
명령을 실행하여 리포지토리의 체크 아웃을 제거합니다. 코드는 RuboCop 결과를 @report
변수에 저장하므로 리포지토리의 체크 아웃을 안전하게 제거할 수 있습니다.
rm -rf
명령은 실행 취소할 수 없습니다. 앱을 안전하게 유지하기 위해 이 자습서의 코드는 수신되는 웹후크에 대해 앱에서 의도한 것과 다른 디렉터리를 제거하는 데 사용할 수 있는 삽입된 악성 명령이 있는지를 검사합니다. 예를 들어 악의적인 작업자가 리포지토리 이름이 ./
인 웹후크를 보내는 경우 앱이 루트 디렉터리를 제거합니다. verify_webhook_signature
메서드는 웹후크의 보낸 사람에 대한 유효성을 검사합니다. 또한 verify_webhook_signature
이벤트 처리기는 리포지토리 이름이 유효한지 검사합니다. 자세한 내용은 "before
필터 정의"를 참조하세요.
코드 테스트
다음 단계에서는 코드가 작동하는지 테스트하고 RuboCop에서 보고한 오류를 보는 방법을 보여 줍니다.
-
다음 명령을 실행하여 터미널에서 서버를 다시 시작합니다. 서버가 이미 실행 중인 경우 먼저 터미널에
Ctrl-C
를 입력하여 서버를 중지한 후 다음 명령을 실행하여 서버를 다시 시작합니다.Shell ruby server.rb
ruby server.rb
-
myfile.rb
파일을 추가한 리포지토리에서 새 끌어오기 요청을 만듭니다. -
서버가 실행 중인 터미널 탭에 린팅 오류가 포함된 디버그 출력이 표시됩니다. 린팅 오류는 형식 없이 인쇄됩니다. 디버그 출력을 복사하여 JSON 포맷터와 같은 웹 도구에 붙여넣어 다음 예시와 같이 JSON 출력의 형식을 지정할 있습니다.
{ "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 } }
2.4단계. RuboCop 오류 수집
@output
변수에는 RuboCop 보고서의 구문 분석된 JSON 결과가 포함됩니다. 이전 단계의 예시 출력과 같이, 결과에는 summary
섹션이 포함되어 있기 때문에 코드가 오류의 유무를 신속하게 확인할 수 있습니다. 다음 코드는 보고된 오류가 없는 경우 검사 실행 결론을 success
로 설정합니다. RuboCop은 files
배열의 각 파일에 대한 오류를 보고하므로 오류가 있는 경우 파일 개체에서 일부 데이터를 추출해야 합니다.
검사 실행을 관리하는 REST API 엔드포인트를 사용하면 특정 코드 줄에 대한 주석을 만들 수 있습니다. 검사 실행을 만들거나 업데이트할 때 주석을 추가할 수 있습니다. 이 자습서에서는 PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
엔드포인트를 사용하여 주석을 포함하는 검사 실행을 업데이트합니다. 이 엔드포인트에 대한 자세한 내용은 "검사 실행에 대한 REST API 엔드포인트" 항목을 참조하세요.
API는 주석 수를 요청당 최대 50개로 제한합니다. 50개 이상의 주석을 만들려면 "검사 실행 업데이트" 엔드포인트에 대한 요청을 여러 차례 수행해야 합니다. 예를 들어 105개의 주석을 만들려면 API에 대해 세 개의 별도 요청을 수행해야 합니다. 처음 두 요청에는 각각 50개의 주석이 있고, 세 번째 요청에는 나머지 5개의 주석이 포함됩니다. 검사 실행을 업데이트할 때마다 검사 실행에 이미 존재하는 주석 목록에 주석이 추가됩니다.
검사 실행에는 주석이 개체 배열로 예상됩니다. 각 주석 개체에는 path
, start_line
, end_line
, annotation_level
및 message
가 포함되어야 합니다. RuboCop도 start_column
및 end_column
을 제공하므로 주석에 선택적 매개 변수를 포함할 수 있습니다. 주석은 동일한 줄에서 start_column
및 end_column
만 지원합니다. 자세한 내용은 "검사 실행에 대한 REST API 엔드포인트"의 annotations
개체를 참조하세요.
이제 코드를 추가하여 각 주석을 만드는 데 필요한 정보를 RuboCop에서 추출합니다.
이전 단계에서 추가한 코드(# ADD ANNOTATIONS CODE HERE #
라고 표시됨) 아래에 다음 코드를 추가합니다.
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 #
이 코드는 주석의 총 수를 50으로 제한합니다. 그러나 각 50개 주석 배치에 대한 검사 실행을 업데이트하도록 이 코드를 수정할 수 있습니다. 위의 코드에는 제한을 50으로 설정하는 변수 max_annotations
가 포함되어 있으며, 공격을 반복하는 루프에서 사용됩니다.
offense_count
가 0이면 CI 테스트는 success
입니다. 오류가 있는 경우 이 코드는 코드 Linter의 오류를 엄격하게 적용하지 않도록 결론을 neutral
로 설정합니다. 그러나 린팅 오류가 있을 때 검사 도구 모음이 실패하도록 하려는 경우 결론을 failure
로 변경할 수 있습니다.
오류가 보고되면 위의 코드는 RuboCop 보고서의 files
배열을 반복합니다. 또한 각 파일에 대해 파일 경로를 추출하고 주석 수준을 notice
로 설정합니다. 사용자는 더 나아가 각 유형의 RuboCop Cop에 대해 특정 경고 수준을 설정할 수 있지만 이 자습서에서 작업을 더 간단하게 유지하려면 모든 오류가 notice
수준으로 설정됩니다.
또한 이 코드는 offenses
배열의 각 오류를 반복하고 공격 및 오류 메시지의 위치를 수집합니다. 필요한 정보를 추출한 후 코드는 각 오류에 대한 주석을 만들어 annotations
배열에 저장합니다. 주석은 동일한 줄의 시작 및 끝 열만 지원하므로 시작 및 끝 줄 값이 동일한 경우에 start_column
및 end_column
이 annotation
개체에만 추가됩니다.
이 코드로는 검사 실행에 대한 주석을 아직 만들지 않습니다. 다음 섹션에서 해당 코드를 추가합니다.
2.5단계. CI 테스트 결과를 사용하여 검사 실행 업데이트
GitHub의 각 검사 실행에는 title
, summary
, text
, annotations
및 images
를 포함하는 output
개체가 포함됩니다. output
에는 summary
및 title
매개 변수만 필요하지만 이 매개 변수만으로는 많은 세부 정보가 제공되지 않으므로 이 자습서에서는 text
와 annotations
도 추가합니다.
summary
의 경우 이 예시에서는 RuboCop의 요약 정보를 사용하고 줄 바꿈(\n
)을 추가하여 출력의 서식을 지정합니다. text
매개 변수에 추가하는 항목을 사용자 지정할 수 있지만 이 예제에서는 text
매개 변수를 RuboCop 버전으로 설정합니다. 다음 코드는 summary
및 text
를 설정합니다.
이전 단계에서 추가한 코드(# ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
라고 표시됨) 아래에 다음 코드를 추가합니다.
# 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']}"
이제 코드에 검사 실행을 업데이트하는 데 필요한 모든 정보가 있어야 합니다. "1.3단계. 검사 실행 업데이트"에서 검사 실행 상태를 success
로 설정하는 코드를 추가했습니다. RuboCop 결과에 따라 설정한 conclusion
변수를 사용하도록 코드를 success
또는 neutral
로 업데이트해야 합니다. 이전에 server.rb
파일에 추가한 코드는 다음과 같습니다.
# Mark the check run as complete!
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: 'success',
accept: 'application/vnd.github+json'
)
코드를 다음 코드로 바꿉니다.
# 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'
)
이제 코드에서 CI 테스트의 상태를 기반으로 결론을 설정하고 RuboCop 결과의 출력을 추가했으므로 CI 테스트 만들기를 완료했습니다.
위의 코드는 actions
개체를 통해 요청된 작업이라는 기능도 CI 서버에 추가합니다. 자세한 내용은 "검사 실행에서 추가 작업 요청"을 참조하세요. 요청된 작업은 GitHub의 검사 탭에 누군가가 추가 작업을 수행하도록 검사 실행을 요청할 수 있도록 하는 단추를 추가합니다. 추가 작업은 앱에서 전부 구성할 수 있습니다. 예를 들어 RuboCop에는 Ruby 코드에서 찾은 오류를 자동으로 수정하는 기능이 있으므로 CI 서버는 요청된 작업 단추를 사용하여 사용자가 자동 오류 수정을 요청할 수 있도록 할 수 있습니다. 누군가가 단추를 클릭하면 앱이 requested_action
동작으로 check_run
이벤트를 받습니다. 요청된 각 작업에는 클릭한 단추를 판단하기 위해 앱에서 사용하는 identifier
가 있습니다.
위의 코드에는 RuboCop이 아직 오류를 자동으로 수정하지 않습니다. 자습서의 뒷부분에서 이를 추가합니다.
코드 테스트
다음 단계에서는 코드가 작동하는지 테스트하고 방금 만든 CI 테스트를 보는 방법을 보여 줍니다.
-
다음 명령을 실행하여 터미널에서 서버를 다시 시작합니다. 서버가 이미 실행 중인 경우 먼저 터미널에
Ctrl-C
를 입력하여 서버를 중지한 후 다음 명령을 실행하여 서버를 다시 시작합니다.Shell ruby server.rb
ruby server.rb
-
myfile.rb
파일을 추가한 리포지토리에서 새 끌어오기 요청을 만듭니다. -
방금 만든 끌어오기 요청에서 검사 탭으로 이동합니다. RuboCop에서 찾은 각 오류에 대한 주석이 표시됩니다. 또한 요청된 작업을 추가하여 만든 “이 문제 해결” 단추를 확인합니다.
2.6단계. RuboCop 오류 자동 수정
지금까지 CI 테스트를 만들었습니다. 이 섹션에서는 발견한 오류를 RuboCop을 사용하여 자동으로 수정하는 기능을 하나 더 추가합니다. 이미 "2.5단계. CI 테스트 결과를 사용하여 검사 실행 업데이트"에 "이 문제 해결" 단추를 추가했습니다. 이제는 누군가 “이 문제 해결” 단추를 클릭할 때 트리거되는 requested_action
검사 실행 이벤트를 처리하는 코드를 추가하겠습니다.
RuboCop 도구는 발견한 오류를 자동으로 수정하는 --auto-correct
명령줄 옵션을 제공합니다. 자세한 내용은 RuboCop 설명서의 "위반 자동 교정"을 참조하세요. --auto-correct
기능을 사용하면 업데이트가 서버의 로컬 파일에 적용됩니다. RuboCop이 수정한 후 변경 내용을 GitHub에 푸시해야 합니다.
리포지토리로 푸시하려면 앱에 리포지토리의 “콘텐츠”에 대한 쓰기 권한이 있어야 합니다. 이미 "2.2단계. RuboCop이 테스트 리포지토리를 복제하도록 허용"에서 해당 권한을 읽기 및 쓰기로 설정했습니다.
파일을 커밋하려면 Git에서 커밋과 연결할 사용자 이름 및 이메일 주소를 알고 있어야 합니다. 다음으로, Git 커밋을 할 때 앱에서 사용할 이름 및 이메일 주소를 저장하는 환경 변수를 추가합니다.
-
이 자습서의 앞부분에서 만든
.env
파일을 엽니다. -
.env
파일에 다음 환경 변수를 추가합니다.APP_NAME
을 앱의 이름으로,EMAIL_ADDRESS
를 이 예시에 사용하려는 이메일로 각각 바꿉니다.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"
다음으로, 환경 변수를 읽고 Git 구성을 설정하는 코드를 추가해야 합니다. 곧 해당 코드를 추가할 것입니다.
누군가가 "이 문제 해결" 단추를 클릭하면 앱에서 작업 유형이 requested_action
인 검사 실행 웹후크를 받습니다.
"1.3단계. 검사 실행 업데이트"에서 server.rb
파일의 event_handler
을(를) 업데이트하여 check_run
이벤트에서 작업을 찾았습니다. created
및 rerequested
작업 형식을 처리할 case 문이 이미 있습니다.
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
rerequested
사례(# ADD REQUESTED_ACTION METHOD HERE #
라고 표시됨) 후 다음 코드를 추가합니다.
when 'requested_action' take_requested_action
when 'requested_action'
take_requested_action
이 코드는 앱에 대한 모든 requested_action
이벤트를 처리하는 새 메서드를 호출합니다.
helpers do
로 시작하는 코드 블록(# ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
라고 표시됨)에서 다음 도우미 메서드를 추가합니다.
# 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
위의 코드는 "2.2단계. RuboCop이 테스트 리포지토리를 복제하도록 허용"에서 추가한 코드와 마찬가지로 리포지토리를 복제합니다. if
문은 요청된 작업의 식별자가 RuboCop 단추 식별자(fix_rubocop_notices
)와 일치하는지 확인합니다. 일치하는 경우 코드는 리포지토리를 복제하고, Git 사용자 이름 및 메일을 설정하고, --auto-correct
옵션을 사용하여 RuboCop을 실행합니다. --auto-correct
옵션은 로컬 CI 서버 파일에 변경 내용을 자동으로 적용합니다.
파일은 로컬로 변경되지만 GitHub(으)로 푸시해야 합니다. ruby-git
gem을 사용하여 모든 파일을 커밋합니다. Git에는 수정되거나 삭제된 모든 파일을 스테이징하고 커밋하는 단일 명령(git commit -a
)이 있습니다. ruby-git
를 사용하여 동일한 작업을 수행하기 위해 위의 코드에서 commit_all
메서드를 사용합니다. 그런 다음 코드는 Git clone
명령과 동일한 인증 방법을 사용하는 설치 토큰을 통해 커밋된 파일을 GitHub(으)로 푸시합니다. 마지막으로 코드는 리포지토리 디렉터리를 제거하여 작업 디렉터리가 다음 이벤트에 대해 준비되었는지 확인합니다.
이제 작성한 코드는 GitHub App 및 검사를 사용하여 빌드한 연속 통합 서버를 완성합니다. 앱의 전체 최종 코드를 보려면 "전체 코드 예시"를 참조하세요.
코드 테스트
다음 단계에서는 코드가 작동하는지, RuboCop에서 찾은 오류를 자동으로 수정할 수 있는지 테스트하는 방법을 보여 줍니다.
-
다음 명령을 실행하여 터미널에서 서버를 다시 시작합니다. 서버가 이미 실행 중인 경우 먼저 터미널에
Ctrl-C
를 입력하여 서버를 중지한 후 다음 명령을 실행하여 서버를 다시 시작합니다.Shell ruby server.rb
ruby server.rb
-
myfile.rb
파일을 추가한 리포지토리에서 새 끌어오기 요청을 만듭니다. -
만든 새 끌어오기 요청에서 검사 탭으로 이동한 후 "이 문제 해결" 단추를 클릭하면 RuboCop에서 찾은 오류가 자동으로 수정됩니다.
-
커밋 탭으로 이동합니다. Git 구성에서 설정한 사용자 이름으로 새 커밋이 표시됩니다. 업데이트를 보려면 브라우저를 새로 고쳐야 할 수 있습니다.
-
검사 탭으로 이동합니다. Octo RuboCop에 대한 새 검사 도구 모음이 표시됩니다. 그러나 이번에는 RuboCop이 모든 오류를 수정했기 때문에 오류가 없어야 합니다.
전체 코드 예시
이 자습서의 모든 단계를 수행한 후 server.rb
의 최종 코드는 다음과 같습니다. 코드 전체에 추가 컨텍스트를 제공하는 주석도 있습니다.
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # If a repository name is provided in the webhook, validate that # it consists only of latin alphabetic characters, `-`, and `_`. unless @payload['repository'].nil? halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil? end authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # 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
다음 단계
이제 API 이벤트를 수신하고, 검사 실행을 만들고, RuboCop을 사용하여 Ruby 오류를 찾고, 끌어오기 요청에 주석을 만들고, Linter 오류를 자동으로 수정하는 앱이 있습니다. 다음으로 앱의 코드를 확장하고, 앱을 배포하고, 앱을 공개할 수 있습니다.
질문이 있는 경우 API 및 웹후크 카테고리에서 GitHub Community 토론을 시작하세요.
앱 코드 수정
이 자습서에서는 리포지토리의 끌어오기 요청에 항상 표시되는 "이 문제 해결" 단추를 만드는 방법을 보여 줍니다. RuboCop에서 오류를 발견한 경우에만 “이 문제 해결” 단추를 표시하도록 코드를 업데이트해 보세요.
RuboCop에서 헤드 분기에 직접 파일을 커밋하지 않으려면 대신 헤드 분기를 기반으로 하는 새 분기를 사용하여 끌어오기 요청 만들기를 수행하도록 코드를 업데이트할 수 있습니다.
앱 배포
이 자습서에서는 로컬에서 앱을 개발하는 방법을 보여 줍니다. 앱을 배포할 준비가 되면 앱을 제공하고 앱의 자격 증명을 안전하게 유지하기 위해 변경해야 합니다. 수행하는 단계는 사용하는 서버에 따라 다르지만 다음 섹션에서는 일반적인 지침을 제공합니다.
서버에서 앱 호스트
이 자습서에서는 컴퓨터 또는 codespace를 서버로 사용했습니다. 앱이 프로덕션 사용을 위해 준비되면 앱을 전용 서버에 배포해야 합니다. 예를 들어 Azure App Service를 사용할 수 있습니다.
웹후크 URL 업데이트
GitHub에서 웹후크 트래픽을 수신하도록 설정된 서버가 있으면 앱 설정에서 웹후크 URL을 업데이트합니다. 프로덕션에서 웹후크를 전달하는 데 Smee.io를 사용해서는 안 됩니다.
:port
설정 업데이트
앱을 배포할 때 서버가 수신 대기하는 포트를 변경할 수 있습니다. 이 코드는 이미 서버에 :bind
를 0.0.0.0
으로 설정하여 사용 가능한 모든 네트워크 인터페이스를 수신 대기하도록 지시합니다.
예를 들어 서버의 .env
파일에 PORT
변수를 설정하여 서버가 수신 대기해야 하는 포트를 나타낼 수 있습니다. 그런 다음 코드가 :port
를 설정하는 위치를 업데이트하여 서버가 배포 포트에서 수신 대기하도록 할 수 있습니다.
set :port, ENV['PORT']
set :port, ENV['PORT']
앱의 자격 증명 보호
앱의 프라이빗 키 또는 웹후크 비밀은 절대 공개해서는 안 됩니다. 이 자습서에서는 앱의 자격 증명을 gitignored .env
파일에 저장했습니다. 앱을 배포할 때 자격 증명을 저장하고 코드를 업데이트하여 값을 적절하게 가져오는 안전한 방법을 선택해야 합니다. 예를 들어 Azure Key Vault와 같은 비밀 관리 서비스를 사용하여 자격 증명을 저장할 수 있습니다. 앱이 실행되면 자격 증명을 검색하여 앱이 배포된 서버의 환경 변수에 저장할 수 있습니다.
자세한 내용은 "GitHub App을 만드는 모범 사례"을(를) 참조하세요.
앱 공유
앱을 다른 사용자 및 조직과 공유하려면 앱을 퍼블릭으로 설정하세요. 자세한 내용은 "공개 또는 비공개 GitHub 앱 만들기"을(를) 참조하세요.
모범 사례 준수
GitHub App을(를) 사용하는 모범 사례를 따르는 것을 목표로 해야 합니다. 자세한 내용은 "GitHub App을 만드는 모범 사례"을(를) 참조하세요.