はじめに
GitHub ActionsはGitHubでCI/CDを支援するべく、GitHubや自前のRunnerを通してビルドおよびテストを行える機能です1。コードの管理とCI/CD環境を一元化できる点は魅力的ですが、Jenkinsには及ばない所も多々あり、自分はJenkinsを使わざるを得ない状況です。
Webhookなどの簡単な連結方式も存在しますが、Jenkins Jobの結果をGitHubで管理したかったのでGitHub ActionsからJenkinsのAPIをポーリングする方向に統合を行いました。一般的なユースケースではないため、ハマったポイントなどを整理しておきます。
目的
- GitHub ActionsでJenkinsのジョブを作動させ、結果を読み込む。
利益
限界
- 結局のところ、JenkinsのAPIをポーリングするだけ。
JenkinsでAPIを使えるようにする
Jenkinsのユーザー設定(http://{jenkins_server_url}/user/{user_id}/configure
)でAPIトークン
を発行します。APIトークン
を使ってAPIを呼ぶ時は下記の二通りの方法があります。
curl -X POST --user "{user_id}:{api_token}" "http://{jenkins_server_url}/job/{job_name}/build"
curl -X POST "http://{user_id}:{api_token}@{jenkins_server_url}/job/{job_name}/build"
GitHub ActionsでJenkinsのAPIを使う
GitHubはプロジェクトディレクトリにある.github/workflowsの下のYMALファイルを認識します2。
Workflowの全体像
name: build
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
pull_request:
types: [review_requested, closed]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
branch:
description: 'Target branch to build'
required: true
default: 'main'
concurrency: build
env:
jenkins_url: ${{secrets.JENKINS_URL}}
jenkins_user: ${{secrets.JENKINS_USER}}
jenkins_token: ${{secrets.JENKINS_TOKEN}}
jobs:
# A single job called "build"
build:
env:
jenkins_job_name: "build_job"
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Extract branch name
shell: bash
run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"
id: extract_branch
- name: Build
shell: bash
run: |
branch_name=${{github.head_ref}}
if [ -z "$branch_name" ]; then branch_name=${{github.event.inputs.branch}}; fi
if [ -z "$branch_name" ]; then branch_name=${{steps.extract_branch.outputs.branch}}; fi
if [ -z "$branch_name" ]; then echo "branch_name is empty"; exit 1; fi
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/buildWithParameters?BRANCH=${branch_name}")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
while ! curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
result=$(curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
if [ "$status" == "SUCCESS" ]; then exit 0; else exit 1; fi
# A single job called "test"
test:
needs: [build]
env:
jenkins_job_name: "test_job"
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Runs a set of commands using the runners shell
- name: Run tests
shell: bash
run: |
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/build")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
while ! curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
result=$(curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
if [ "$status" == "SUCCESS" ]; then exit 0; else exit 1; fi
グローバル設定の仕方
on
on
ではいつワークフローが作動するかを宣言します3。上記の例ではmainブランチにプッシュした時、PRでレビューを要請した時、PRを閉じる時に自動的にワークフローが作動するようにしています。またworkflow_dispatch
によって手動で作動させることも許可しています。その場合はbranch
という値を利用してどのブランチをビルドするかを決定するようにしています。
concurrency
concurrency
オプションで同じ値を保持しているワークフローは同時に実行されません。Jenkinsでもジョブ毎に並行実行に関する設定はできますが、このGitHub Actionsはワークフロー自体がJenkinsのpipelineに相当するものですのでpipelineの並行実行を禁止したい時に必要になります。
env
ワークフローで使用するグローバル変数です。ジョブごとにも宣言できます。ここではJenkins APIを叩くときに使う値をGitHub Secretsから持ってきます4。基本的にCIのログはオープンされますのでAPIのUSERとTOKENは必ずSecretsを使うべきです。
ジョブ設定の仕方
ジョブは基本的にシェル環境で実行するコマンドを順に並べる感じです。下記の例はbuild
とtest
という二つのジョブを宣言しています。
need
need
はジョブの間の依存関係を表現します。上記の例ではbuild
が実行された後test
が実行されます。
branch名の読み取り方
- name: Extract branch name
shell: bash
run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"
id: extract_branch
- name: Build
shell: bash
run: |
branch_name=${{github.head_ref}}
if [ -z "$branch_name" ]; then branch_name=${{github.event.inputs.branch}}; fi
if [ -z "$branch_name" ]; then branch_name=${{steps.extract_branch.outputs.branch}}; fi
if [ -z "$branch_name" ]; then echo "branch_name is empty"; exit 1; fi
Github Actionsはトリガーによってbranch名を取得する方法が異なります。PRでトリガーされた時は${{github.head_ref}}
を読めないいのですが、プッシュされたときはExtract branch nameステップを通して名前を読み取る必要があります5。手動で実行させる時は床からも値がきませんので上記のworkflow_dispatch.inputs
で強制した値を利用します。
Jenkinsとの繋げ方
JenkinsのジョブをAPIから実行
基本的にはトークンと一緒にPOST
でREST APIを叩くだけです。パラメーターがある場合はbuildWithParameters
をない場合はbuild
を使います。-w "\n%{size_header},%{size_download}"
はヘッダーを読み取る時に使います。最後にヘッダー利用して要請が正常的に実行されたかを確認します。
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/build")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
response=$(curl -si -w "\n%{size_header},%{size_download}" -X POST --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/buildWithParameters?BRANCH=${branch_name}")
header_size=$(sed -n '$ s/^\([0-9]*\),.*$/\1/ p' <<< "${response}")
headers="${response:0:${header_size}}"
if ! echo ${headers} | grep -q "HTTP/2 201"; then exit 1; fi
Jenkinsのジョブが完了するまで待機
Jenkinsのジョブを実行するとtarget queue id
という値が返ってきます。これがJenkinsがジョブに付与するグローバルな管理用IDです。下記のAPIを使うことで該当するジョブが完了したかいなか確認できます。下のコードはポーリングの一例になります。
target_queue_id=$(sed -n 's/^.*item\/\([0-9]*\).*$/\1/ p' <<< "${headers}")
while ! curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]" | grep \<result\> > /dev/null; do echo "Waiting..."; sleep 15s; done;
ジョブの成否を読み取る
ポーリングが終わったらポーリングと同じAPIを通してジョブの結界を読み取ることができます。
ジョブの成否をGitHub Actionsに反映するために結果をリターンコードに変換します。
result=$(curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
status=$(sed -n 's/^.*<result>\([A-Z]*\)<\/result>.*$/\1/ p' <<< "${result}")
if [ "$status" == "SUCCESS" ]; then exit 0; else exit 1; fi
おまけ:Jenkinsジョブのコンソールを読み取る
Jenkinsのjob/${{ env.jenkins_job_name }}/${build_id}/consoleText
を叩けばコンソールログを読み取ることもできます。
result=$(curl --silent --globoff --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/api/xml?
tree=builds[id,number,result,queueId]&xpath=//build[queueId=$target_queue_id]")
build_id=$(sed -n 's/^.*<id>\([0-9]*\)<\/id>.*$/\1/ p' <<< "${result}")
curl -i -X POST --user "${{ env.jenkins_user }}:${{ env.jenkins_token }}" "${{ env.jenkins_url }}/job/${{ env.jenkins_job_name }}/${build_id}/consoleText"
-
GitHub Actions, https://docs.github.com/ja/actions ↩
-
ワークフロー用のYAML構文について, https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows ↩
-
workflow-syntax-for-github-actions#on, https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions#on ↩
-
暗号化されたシークレット, https://docs.github.com/ja/actions/security-guides/encrypted-secrets ↩
-
GitHub Actionsでブランチ名を扱う方法, https://qiita.com/iery/items/43b72813c394050b8bbc ↩