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 아래에 단계를 - 기호를 사용하여 리스트(list) 형식으로 나열합니다. 커맨드나 스크립트를 실행할 때는 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 ci
      - run: npm test

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

단계 간 출력값 전달

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

출력 매개변수로 값을 쓰려면 변수 이름과 값을 {name}={value} 형태로 $GITHUB_OUTPUT이라는 환경 파일로 전달해야하고, 출력 매개변수로 값을 읽으려면 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 "foo=bar" >> "$GITHUB_OUTPUT"
      - run: echo ${{ steps.set-foo.outputs.foo }}
foobar
☑️ Set up Job
☑️ Run echo "foo=bar" >> "$GITHUB_OUTPUT"
☑️ 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 "num=$(($RANDOM % 10 + 1))" >> "$GITHUB_OUTPUT"
      - id: gen-num2
        run: echo "num=$(($RANDOM % 10 + 1))" >> "$GITHUB_OUTPUT"
      - 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 "num=$(($RANDOM % 10 + 1))" >> "$GITHUB_OUTPUT"
☑️ Run echo "num=$(($RANDOM % 10 + 1))" >> "$GITHUB_OUTPUT"
☑️ 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

단계(step) 뿐만 아니라 작업(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 "num=$(($RANDOM % 2))" >> "$GITHUB_OUTPUT"
      - 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 "num=$(($RANDOM % 2))" >> "$GITHUB_OUTPUT"
🚫 Run echo zero☑️ Run echo one☑️ Complete Job

불안정한 단계의 실패 무시

GitHub Actions에서는 기본적으로 작업(job) 실행 도중에 어떤 단계(step)가 실패하면 그 이후의 단계는 실행되지 않고 작업이 중단되는데요. 대부분의 경우, 이러한 기본 실행 방식이 워크플로우 실행 시간을 단축하고 불필요한 CI 서버 리소스를 줄이는데 도움이 되기 때문에 합리적으로 여겨집니다.

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

이런 경우를 대비해서 단계(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: exit $(($RANDOM % 2 == 0))
      - if: always()
        run: echo ${{ steps.random.outcome }}

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

notify
☑️ Set up Job
☑️ Run exit $(($RANDOM % 2 == 0))
☑️ Run echo success
▶ Run echo successsuccess☑️ Complete Job

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

notify
☑️ Set up Job
❌  Run exit $(($RANDOM % 2 == 0))
▶ Run exit $(($RANDOM % 2 == 0))
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

실습 코드

본 포스팅에서 작성한 YAML 파일과 워크플로우 실행 결과는 아래 코드 저장소에서 확인하실 수 있습니다.

마치면서

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

GitHub Actions 관련 포스팅은 GitHub Actions 태그를 통해서 쉽게 만나보세요!