Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

[ci skip] 커밋 메시지로 GitHub Actions 실행 취소하기

CI를 사용하면 커밋이 올라올 때마다 CI를 실행하게 되는데 보통 시간도 몇 분 이상 걸리고 유료로 사용하면 과금이 되기 때문에 커밋을 올리면서 불필요한 CI 실행을 하지 않을 필요가 있다. 예를 들면 README 파일만 수정했다거나 소스 코드와 관련이 없는 부분의 수정이라서 굳이 CI를 실행해서 결과를 확인해 보지 않아도 되는 경우이다.

개발자가 직접 취소할 수도 있지만, 대부분의 CI에서는 커밋 메시지에 [ci skip][skip ci]를 추가하면 CI 실행을 하지 않고 있다. Travis CI도 이를 지원하고 CircleCI도 지원하고 있다.

일반화된 기능이라 GitHub Actions에도 있을 것 같지만 아직 이를 지원하지 않는다. 그래서 검색을 해보면 커뮤니티에서는 if: "! contains(toJSON(github.event.commits.*.msg), '[skip-ci]')" 같은 설정으로 이를 할 수 있다고 하고 있지만 실제로 사용해보면 이걸로는 다 되진 않는다.

얼마 전 이 부분이 필요해서 작업했는데 당시에는 기존의 마켓플레이스에 올라와 있던 Skip based on commit message가 더이상 유지 보수되고 있지 않아서 직접 적용했는데 글을 쓰면서 찾아보니 11월에 Skip Workflow, CI-SKIP-ACTION 2개의 액션이 올라와 있다. 이 2개의 코드를 다 열어보진 않았는데 아주 간단히 사용하려면 위 액션을 참고하면 좋을 것 같다.

GitHub Actions에서 [ci skip]

다음과 같은 간한 CI 설정이 있다고 해보자. 여기서는 test라는 메인 job 1개만 있다.

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: 14.x
    - run: npm test

skip 기능을 검색해 보면 if를 사용해서 커밋 검사하는 방법을 다들 대답하고 있는데 제대로 안 해본 게 틀림없다. if 구문은 jobs.<job_id>.if에서 쓸 수 있는데 해당 조건이 참이면 잡을 실행 한다. 여기서 사용하는 표현식은 GitHub 문서를 참고하면 된다.

jobs:
  test:
    runs-on: ubuntu-latest
    if: ${{ !contains(github.event.head_commit.message, '[ci skip]') }}
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: 14.x
    - run: npm test

if 부분을 추가했다. Git은 여러 커밋을 한꺼번에 올리는 경우가 많은데 보통 마지막 커밋에 대해서만 CI를 실행하므로 헤드 커밋 메시지(github.event.head_commit.message)에 [ci skip] 문자열이 포함되어 있는지 검사해서 포함되어 있지 않으면 test 잡을 실행 한다. 만약 [skip ci]도 지원하고 싶다면 다음과 같이 조건을 하나 더 붙여주면 된다.

if: >-
  ${{ !contains(github.event.head_commit.message, '[ci skip]') &&
  !contains(github.event.head_commit.message, '[skip ci]')
  }}

GitHub Action이 취소됨

[ci skip] 메시지가 있는 커밋을 올리면 해당 잡이 실행 안 된 것을 확인할 수 있다.

리스트에서 취소된 잡이 취소표시됨

잡이 1개뿐이라 워크플로우 자체가 실행 안되었기 때문에 리스트에서도 취소된 것을 볼 수 있다.

Pull Request에서 [ci skip]

같은 방식으로 [ci skip]이 메시지에 포함된 커밋을 Pull Request로 올려보자.

Push에서는 잘 skip되지만 Pull Request에서는 실행됨

테스트한 저장소가 포크한 저장소가 아니라 브랜치도 푸시 되었기 때문에 push 이벤트와 pull_request 이벤트 모두에 실행되었다. 위 결과를 보면 push 이벤트에서는 제대로 실행을 건너뛰었지만 pull_request 이벤트에서는 그냥 실행되었다.

debug라는 잡은 일부러 github.event.head_commit.message를 출력해보려고 추가한 것인데 우측을 보면 echo로 아무것도 출력되지 않은 걸 볼 수 있다. GitHub에서 push 이벤트의 페이로드pull_request 이벤트의 페이로드가 완전히 다르기 때문이다. pull_request 이벤트에서는 head_commit을 주지 않아서 커밋 메시지를 알아낼 수가 없었다.

pull_request 정보를 가지고 API로 커밋 내용을 가져오거나 코드를 체크아웃해서 커밋메시지를 확인해야 했다. [ci skip] 기능이 Pull Request에서도 필요했기 때문에 나는 코드를 체크아웃해서 가져오는 방법을 이용했다.

Pull Request에서 [ci skip]을 확인하려다 보니 꽤 복잡해졌다. 앞에서 다른 사람들이 만든 액션이 있어서 그걸 써도 되지만 여기서 GitHub Actions 사용 방법을 몇 가지 볼 수 있다.

jobs:
  prepare-commit-msg:
    name: Retrive head commit message
    runs-on: ubuntu-latest
    outputs:
      HEAD_COMMIT_MSG: ${{ steps.commitMsg.outputs.HEAD_COMMIT_MSG }}
    steps:
      - uses: actions/checkout@v1
        if: github.event_name == 'pull_request'
      - name: find commit msg for PR
        id: commitMsg
        if: github.event_name == 'pull_request'
        run: echo "::set-output name=HEAD_COMMIT_MSG::$(git log --no-merges -1 --oneline)"
  check-skip:
    name: Check to skip CI
    needs: prepare-commit-msg
    runs-on: ubuntu-latest
    if: ${{ !contains(github.event.head_commit.message, '[ci skip]') && !contains(needs.prepare-commit-msg.outputs.HEAD_COMMIT_MSG, '[ci skip]') }}
    steps:
      - run: echo "${{ github.event.head_commit.message }}"
  test:
    runs-on: ubuntu-latest
    needs: check-skip
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v1
      with:
        node-version: 14.x
    - run: npm test
  • prepare-commit-msgcheck-skip 2개의 잡을 추가 했다.
  • 3개의 잡을 needs로 연결했다. needs로 다른 잡을 지정 하면 해당 잡이 성공해야만 다음 잡이 실행된다.
  • prepare-commit-msgpull_request인 경우 커밋 메시지를 찾는 잡이다.

    • 모든 스텝에서 if: github.event_name == 'pull_request'를 추가해서 다른 이벤트에서는 실행되지 않도록 했다. if를 잡에 걸지 않은 이유는 이 잡이 취소되면 이어진 잡이 실행되지 않기 때문이다.
    • echo "::set-output name=HEAD_COMMIT_MSG::$(git log --no-merges -1 --oneline)"::set-output 커맨드를 이용해서 헤드 커밋의 메시지를 HEAD_COMMIT_MSG에 저장한 것이다. 이렇게 내보내면 같은 잡에서 이 값을 참조해서 사용할 수 있다.
    • 잡에 outputs:를 따로 지정했다. 앞에서 set-output을 지정했지만, 이 출력값은 다른 잡에서는 참조를 할 수 없다. 그래서 steps.commitMsg.outputs.HEAD_COMMIT_MSG를 가져와서 잡의 출력값으로 다시 지정한 것이다.
  • check-skip 잡은 스킵할 지 여부를 정하는 잡이다.

    • if 구문이 더 복잡해졌는데 !contains(github.event.head_commit.message, '[ci skip]')는 앞에서 설명한 대로 push 이벤트일 때 바로 여기서 스킵 여부를 결정할 수 있다.
    • pull_request 이벤트일 때 뒤 조건인 !contains(needs.prepare-commit-msg.outputs.HEAD_COMMIT_MSG, '[ci skip]')에서 판단하게 된다. needs로 이전 잡을 연결해 왔기 때문에 출력값을 needs.prepare-commit-msg.outputs.HEAD_COMMIT_MSG로 가져올 수 있다. 앞에서 Pull Request의 헤드 커밋 메시지를 찾아놨기 때문에 여기서는 검사만 한다.
    • 이 잡은 실행여부 검사만 하기 때문에 스텝에서는 헤드 커밋을 echo로 출력하기만 했다. test잡에서 needs: check-skip로 지정했으므로 이 잡이 실행되어야 전체를 실행할 수 있다.

Push와 Pull Request에서 모두 잘 skip 됨

다시 올린 Pull Reqeust를 보면 push, pull_request 둘 다 실행이 잘 취소된 것을 볼 수 있다. Retrive head commit message 잡이 실행되는 것은 별수 없다.

2020/11/22 19:50 2020/11/22 19:50