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
속성을 통해서 명시할 수 있습니다.
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
에 저장된 값을 읽어와 콘솔에 출력하고 있습니다.
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 }}
☑️ Set up Job
☑️ Run echo "foo=bar" >> "$GITHUB_OUTPUT"
☑️ Run echo bar
▶ Run echo barbar☑️ Complete Job
다른 예로, 첫 번째 단계와 두 번째 단계에서 무작위 숫자를 생성한 후, 그 이후 단계에서 두 숫자를 가지고 사칙 연산을 해볼까요?
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 }}))
☑️ 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
이면 세 번째 단계가 수행하는 작업을 셋업해보겠습니다.
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
이라면 아래와 같이 두 번째 단계는 생략되어 수행이 안 되고, 세 번째 단계만 수행되는 것을 볼 수 있을 것입니다.
☑️ 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!
가 콘솔에 출력됩니다.
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!"
☑️ 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) 사용하고 있습니다.
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
가 콘솔에 출력될 것입니다.
☑️ Set up Job
☑️ Run exit $(($RANDOM % 2 == 0))
☑️ Run echo success
▶ Run echo successsuccess☑️ Complete Job
하지만 첫 번째 단계가 실패했다면, 두 번째 단계에서 failure
가 콘솔에 출력될 것입니다.
☑️ 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)을 설정해주면 됩니다.
예를 들어, 첫 번째 단계를 무조건 실패하게 하고, 두 번째 단계가 대신 수행되도록 작업 설정해보겠습니다.
name: Our Steps
on: push
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: original
run: exit 1
- name: backup
if: failure()
run: echo backup
☑️ Set up Job
❌ original
▶ Run exit 1
Error: Process completed with exit code 1.
☑️ backup
▶ Run echo backupbackup☑️ Complete Job
이번에는 첫 번째 단계가 무조건 통과하게 워크플로우를 수정해볼까요?
name: Our Steps
on: push
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: original
run: exit 0
- name: backup
if: failure()
run: echo backup
이번에는 두 번째 단계가 수행되지 않은 것을 볼 수 있습니다.
☑️ Set up Job
☑️ original
▶ Run exit 0
🚫 backup☑️ Complete Job
실습 코드
본 포스팅에서 작성한 YAML 파일과 워크플로우 실행 결과는 아래 코드 저장소에서 확인하실 수 있습니다.
- Code: https://github.com/DaleSchool/github-actions-steps
- Actions: https://github.com/DaleSchool/github-actions-steps/actions
마치면서
이상으로 GitHub Actions에서 단계(step)의 수행을 제어하는 다양한 방법에 대해서 살펴보았습니다. 의도치 않게 GitHub Actions의 문맥(context), 표현식(expression)에 대해서도 살짝 다루게 되었는데요. 이 부분에 대해서는 추후 별도의 포스팅을 통해서 자세히 다루면 좋을 것 같습니다.
GitHub Actions 관련 포스팅은 GitHub Actions 태그를 통해서 쉽게 만나보세요!