Logo

GitHub Actions 단계(step) 고급 설정

지난 포스팅에서는 GitHub Actions의 4가지 핵심적인 개념인 워크플로우(workflow), 작업(job), 단계(step), 액션(action)에 대해서 가볍게 살펴보았는데요.

이번 포스팅에서는 작업(Job)의 근간이 되는 단계(step)에 대해서 좀 더 깊이 다뤄보도록 하겠습니다.

GitHub Actions에서 단계(step)이란?

GitHub Actions에서 하나의 작업(job)은 순차적으로 실행되는 여러 단계(step)로 모델링이 되는데요. 이 단계는 단순한 커맨드(command)나 스크립트(script)가 될 수도 있고 액션(action)이라고 하는 좀 더 복잡한 명령 단위일 수도 있습니다.

워크플로우 파일에서는 jobs.<job_id>.steps 아래에 단계를 - 기호를 사용하여 리스트 형식으로 나열합니다. 커맨드나 스크립트를 실행할 때는 run 속성을 사용하며, 액션을 사용할 때는 uses 속성을 사용합니다.

예를 들어 자바스크립트 프로젝트에서 테스트를 돌리려면 CI 서버로 코드를 내려 받고, npm 패키지를 설치한 후, 테스트를 실행해야할텐데요. 이 3단계의 작업은 아래와 같이 steps 속성을 통해서 명시할 수 있습니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm install
      - run: npm test

서로 격리된 환경, 즉 독립된 CI 서버에서 돌아가는 작업(job)과 달리, 단계(step)는 동일한 CI 서버에서 순차적으로 수행됩니다. 따라서 이전 단계의 처리 결과를 다음 단계에서 활용할 수 있는 특징을 가지고 있습니다.

단계 간 출력 값 전달

단계는 순차적으로 수행되기 때문에 이전 단계에서 발생한 결과물을 다음 단계로 전달하는 것이 가능합니다. 즉, 어떤 단계에서 특정 값을 출력으로 내보내면 그 단계 이후로 실행되는 모든 단계에서 해당 출력 값을 불러올 수 있습니다.

출력 변수의 값을 쓰려면 GitHub Actions의 명령어(command)인 ::set-output name={name}::{value} 문법을 사용해야하고, 출력 변수의 값을 읽으려면 GitHub Actions의 명령어(command)인 GitHub Actions의 문맥(context)인 steps.<step_id>.outputs.<output_name> 문법을 사용해야합니다.

예를 들어, 다음과 같이 2 단계(step)로 이뤄진 작업(job)을 생각해볼까요? 첫 번째 단계에서는 foo라는 이름으로 bar라는 값을 출력(output)을 쓰고 있고 두 번째 단계에서는 foo에 저장된 값을 읽어와 콘솔에 출력하고 있습니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  foobar:
    runs-on: ubuntu-latest
    steps:
      - id: set-foo
        run: echo "::set-output name=foo::bar"
      - run: echo ${{ steps.set-foo.outputs.foo }}
foobar
☑️ Set up Job
☑️ Run echo "::set-output name=foo::bar"
☑️ Run echo bar
▶ Run echo barbar☑️ Complete Job

다른 예로, 첫 번째 단계와 두 번째 단계에서 무작위 숫자를 생성한 후, 그 이후 단계에서 두 숫자를 가지고 사칙 연산을 해볼까요?

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  calculate:
    runs-on: ubuntu-latest
    steps:
      - id: gen-num1
        run: echo "::set-output name=num::$(($RANDOM % 10 + 1))"
      - id: gen-num2
        run: echo "::set-output name=num::$(($RANDOM % 10 + 1))"
      - run: echo $((${{ steps.gen-num1.outputs.num }} + ${{ steps.gen-num2.outputs.num }}))
      - run: echo $((${{ steps.gen-num1.outputs.num }} - ${{ steps.gen-num2.outputs.num }}))
      - run: echo $((${{ steps.gen-num1.outputs.num }} * ${{ steps.gen-num2.outputs.num }}))
      - run: echo $((${{ steps.gen-num1.outputs.num }} / ${{ steps.gen-num2.outputs.num }}))
foobar
☑️ Set up Job
☑️ Run echo "::set-output name=num::$(($RANDOM % 10 + 1))"
☑️ Run echo "::set-output name=num::$(($RANDOM % 10 + 1))"
☑️ Run echo $((5 + 9))
▶ Run echo $((5 + 9))14☑️ Run echo $((5 - 9))
▶ Run echo $((5 - 9))-4☑️ Run echo $((5 * 9))
▶ Run echo $((5 * 9))45☑️ Run echo $((5 / 9))
▶ Run echo $((5 / 9))0☑️ Complete Job

단계의 선택적 수행

작업(job)을 if 속성을 통해 실행 여부를 통제하는 것 것처럼 단계(step) 수준에서도 if 속성을 사용할 수 있습니다.

GitHub Actions의 작업(job)에 대한 자세한 설명은 관련 포스팅을 참고 바랍니다.

예를 들어, 첫 번째 단계에서 0 또는 1을 무작위로 생성하고, 그 결과가 0이면 두 번째 단계, 1이면 세 번째 단계가 수행하는 작업을 셋업해보겠습니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  zeroone:
    runs-on: ubuntu-latest
    steps:
      - id: gen-num
        run: echo "::set-output name=num::$(($RANDOM % 2))"
      - if: steps.gen-num.outputs.num == 0
        run: echo zero
      - if: steps.gen-num.outputs.num == 1
        run: echo one

만약에 첫 번째 단계에서 생성한 숫자가 1이라면 아래와 같이 두 번째 단계는 생략되어 수행이 안 되고, 세 번째 단계만 수행되는 것을 볼 수 있을 것입니다.

zeroone
☑️ Set up Job
☑️ Run echo "::set-output name=num::$(($RANDOM % 2))"
🚫 Run echo zero☑️ Run echo one☑️ Complete Job

불안정한 단계의 실패 무시

GitHub Actions에서는 기본적으로 작업(job) 실행 도중에 어떤 단계(step)가 실패하면 그 이후의 단계는 실행되지 않고 작업이 중단되는데요. 대부분의 경우 이러한 GitHub Actions의 기본 처리 방식이 불필요한 단계를 생략할 수 있어서 합리적으로 여겨집니다.

하지만 실제 프로젝트에서는 성패가 오락가락하는 불안정한 단계가 있을 수 있죠? 대표적인 예로, 테스트 케이스 중에서 성패 여부를 종잡을 수 없는 녀석이 있을 수 있는데요. 이런 상황에서 테스트 단계가 실패할 때 마다 해당 작업 전체가 중단된다면 팀 전체가 곤란해질 것입니다.

이런 경우를 대비해서 단계(step)는 continue-on-error 속성을 지원하는데요. 이 속성을 true로 설정해줄 경우, 해당 단계가 실패하더라도 작업은 중단되지 않고 남은 단계를 계속해서 실행해 줍니다.

예를 들어, 다음 작업에서 첫 번째 단계는 항상 실패하게 되지만 continue-on-error 속성이 true로 설정되어 있기 때문에 두 번째 단계에서 I don't care!가 콘솔에 출력됩니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  ignore:
    runs-on: ubuntu-latest
    steps:
      - id: flaky
        continue-on-error: true
        run: exit 1
      - run: echo "I don't care!"
ignore
☑️ Set up Job
☑️ Run exit 1
☑️ Run echo "I don't care!"
▶ Run echo "I don't care!"I don't care!☑️ Complete Job

이전 단계의 성패 여부와 상관없이 다음 단계 수행

만약에 이전 단계의 성패 여부와 상관없이 무조건 수행되야 하는 단계가 있으면 어떻게 해야할까요? 흔한 사례로, 작업의 실행 결과를 이메일이나 메세징 애플리케이션으로 통보해야할 때를 들 수 있겠네요. 작업의 실행 결과가 성공이든 실패든 통보를 받고 싶을테니까요.

이럴 경우에는 무조건 수행되야하는 단계의 if 속성에 always()라는 GitHub Actions의 표현식(expression)을 설정해주면 되는데요.

예를 들어, 첫 번째 단계를 랜덤하게 성공하거나 실패하게 한 다음에, 두 번째 단계에서 항상 첫 번째 단계의 결과가 출력되도록 작업 설정을 해보겠습니다. 특정 단계의 출력 결과를 확인하기 위해서 steps.<step_id>.outcome이라는 GitHub Actions의 문맥(context) 사용하고 있습니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - id: random
        run: if [[ $(($RANDOM % 2)) == 0 ]]; then exit 0; else exit 1; fi
      - if: ${{ always() }}
        run: echo ${{ steps.random.outcome }}

만약에 첫 번째 단계가 성공했다면, 두 번째 단계에서 success가 콘솔에 출력될 것입니다.

notify
☑️ Set up Job
☑️ Run if [[ $(($RANDOM % 2)) == 0 ]]; then exit 0; else exit 1; fi
☑️ Run echo success
▶ Run echo successsuccess☑️ Complete Job

하지만 첫 번째 단계가 실패했다면, 두 번째 단계에서 failure가 콘솔에 출력될 것입니다.

notify
☑️ Set up Job
❌  Run if [[ $(($RANDOM % 2)) == 0 ]]; then exit 0; else exit 1; fi
▶ Run if [[ $(($RANDOM % 2)) == 0 ]]; then exit 0; else exit 1; fi
Error: Process completed with exit code 1.
☑️ Run echo failure
▶ Run echo failurefailure☑️ Complete Job

이를 통해 우리는 첫 번째 단계의 결과가 어찌됐든 무조건 두 번째 단계가 수행되는 것을 알 수 있습니다.

이전 단계가 실패했을 때만 다음 단계 수행

간혹, 어떤 단계가 실패했을 때만 예비로 수행될 백업(backup) 단계를 설정해야 될 때가 있는데요. 이 경우에는 해당 백업 단계의 if 속성에 failure()라는 GitHub Actions의 표현식(expression)을 설정해주면 됩니다.

예를 들어, 첫 번째 단계를 무조건 실패하게 하고, 두 번째 단계가 대신 수행되도록 작업 설정해보겠습니다.

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: original
        run: exit 1
      - name: backup
        if: ${{ failure() }}
        run: echo backup
backup
☑️ Set up Job
❌ original
▶ Run exit 1
Error: Process completed with exit code 1.
☑️ backup
▶ Run echo backupbackup☑️ Complete Job

이 번에는 첫 번째 단계가 무조건 통과하게 워크플로우를 수정해볼까요?

.github/workflows/steps.yml
name: Our Steps
on: push
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: original
        run: exit 0
      - name: backup
        if: ${{ failure() }}
        run: echo backup

이 번에는 두 번째 단계가 수행되지 않은 것을 볼 수 있습니다.

backup
☑️ Set up Job
☑️ original
▶ Run exit 0
🚫 backup☑️ Complete Job

마치면서

이상으로 GitHub Actions에서 단계(step)의 수행을 제어하는 다양한 방법에 대해서 살펴보았습니다. 의도치 않게 GitHub Actions의 명령어(command), 문맥(context), 표현식(expression)에 대해서도 살짝 다루게 되었는데요. 이 부분에 대해서는 추후 별도의 포스팅을 통해서 자세히 다루면 좋을 것 같습니다.