1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GitHub ActionsからJenkinsのCIジョブを利用する

Last updated at Posted at 2021-11-30

はじめに

GitHub ActionsはGitHubでCI/CDを支援するべく、GitHubや自前のRunnerを通してビルドおよびテストを行える機能です1。コードの管理とCI/CD環境を一元化できる点は魅力的ですが、Jenkinsには及ばない所も多々あり、自分はJenkinsを使わざるを得ない状況です。

Webhookなどの簡単な連結方式も存在しますが、Jenkins Jobの結果をGitHubで管理したかったのでGitHub ActionsからJenkinsのAPIをポーリングする方向に統合を行いました。一般的なユースケースではないため、ハマったポイントなどを整理しておきます。

目的

  • GitHub ActionsでJenkinsのジョブを作動させ、結果を読み込む。

利益

  • PRで作業するとき、自動でJenkinsでテストなどが行われる。
  • GitHubからJenkinsの細やかな機能が利用できる。
  • GitHub上でPipelineの流れが把握できる。
    例1.png

限界

  • 結局のところ、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の全体像

.github/workflows/build.yml
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を使うべきです。

ジョブ設定の仕方

ジョブは基本的にシェル環境で実行するコマンドを順に並べる感じです。下記の例はbuildtestという二つのジョブを宣言しています。

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"
  1. GitHub Actions, https://docs.github.com/ja/actions

  2. ワークフロー用のYAML構文について, https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows

  3. workflow-syntax-for-github-actions#on, https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions#on

  4. 暗号化されたシークレット, https://docs.github.com/ja/actions/security-guides/encrypted-secrets

  5. GitHub Actionsでブランチ名を扱う方法, https://qiita.com/iery/items/43b72813c394050b8bbc

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?