들어가며
GitHub Actions 시리즈를 시작하고 꾸준히 작성해보려 했는데, 현생과 다른 주제의 글을 적다보니 생각보다 너무 늦어졌네요. 🥲
그래서 오랜만에 다시 돌아왔습니다!
이전 포스팅에서는 GitHub Actions에 대한 개념과 CI를 간단하게 적용하는 가이드를 작성했습니다.
이전 포스팅을 읽고 오시는 것을 추천드립니다!
[Android] GitHub Actions로 CI 적용하기 - 간단 가이드
들어가며여러 개발자와 협업을 할 때, CI(Continuous Integration)나 CD(Continuous Deployment)가 적용되어있다면 각자가 개발한 기능을 병합하기 수월해집니다.CI/CD를 지원해주는 도구는 젠킨스(Jenkins), 서클C
walnut-dev.tistory.com
이번 포스팅에서는 Secret을 활용해 원격 레포지토리로의 Push 없이도 필요한 설정 데이터를 가져오고, 몇 가지 유용한 CI Action들을 소개하여 CI 워크플로를 다듬어보도록 하겠습니다.
Secret 사용하기
Using secrets in GitHub Actions - 공식문서
Using secrets in GitHub Actions - GitHub Docs
Secrets allow you to store sensitive information in your organization, repository, or repository environments.
docs.github.com
사실 공식 문서에 잘 소개되어 있지만, 처음이신 분들이 헤매지 않도록 초심자 입장에서 좀 더 친절하고 자세히 안내해보겠습니다.
(제가 그랬듯이요🫠)
Secret 이란?
우선 개념부터 짚어가봅시다!
https://docs.github.com/en/actions/concepts/security/about-secrets
About secrets - GitHub Docs
Secrets allow you to store sensitive information in your organization, repository, or repository environments. Secrets are variables that you create to use in GitHub Actions workflows in an organization, repository, or repository environment. GitHub Action
docs.github.com
About secrets
Secret을 사용해 Organization, Repository와 Repository Environments에서 다루는 민감한 정보를 저장할 수 있습니다. Secret은 GitHub Actions 워크플로에서 사용하기 위해 만드는 변수입니다.
GitHub Actions가 Secret을 읽을 수 있으려면 워크플로에 명시적으로 Secret을 포함시켜야 합니다.
Secret은 민감한 정보를 암호화하여 저장하고, 이를 워크플로에서 사용할 수 있는 변수입니다.
단순한 프로젝트라면 무관하지만, 만약 Google Maps와 같은 외부 SDK를 추가로 사용하고 계시다면 아래 설정 파일이 필요할 수 있습니다.
- API Key를 저장하고 있는 properties 파일(secret.properties, local.defaults.properties 등)
- 서비스 계정 키 파일(google-service.json 등)
이러한 파일들은 외부에 함부로 유출되지 않아야 하며, 원격에 절대로 올려서는 안되는 파일입니다. 하지만 프로젝트를 빌드하는데 필요합니다.
보안 상 중요한 데이터를 저장하고, CI/CD 워크플로에서 안전하게 가져와 사용할 수 있게 지원해주는 기능이 바로 Secret 입니다.
Secret은 접근 범위에 따라 세 종류로 나뉘어집니다.
- Organization secret
- 여러 Repository에서 공통으로 사용하며, 한 곳에서 갱신이 가능합니다.
- 특정 Repository 집합에 한해 접근을 제한할 수 있습니다.
- Repository secret
- 현재 Repository에서만 사용할 수 있습니다.
- 일반적으로 가장 많이 사용합니다.
- Environment secret
- Repository 내에서 사용되며, prod, staging 등 배포를 단계별로 분리할 수 있습니다.
- 필수 승인자가 승인한 후에만 접근하여 사용할 수 있습니다.
이 글에서는 Repository secret을 다루는 방법을 알아봅니다.
Secret 만들기
워크플로에서 Secret을 사용하려면 먼저 Secret 변수를 만들어주어야겠죠?
Secret을 생성하는 방법은 UI(GitHub 웹페이지), GitHub CLI(Command Line Interface), REST API 세 가지가 있습니다.
지금은 간단하게 UI에서 생성하는 방법을 알아보겠습니다.
GitHub Docs - Using secrets in GitHub Actions
Using secrets in GitHub Actions - GitHub Docs
To create secrets or variables on GitHub for a personal account repository, you must be the repository owner. To create secrets or variables on GitHub for an organization repository, you must have admin access. Lastly, to create secrets or variables for a
docs.github.com
1. Secret 설정 화면 이동
Repository Secret을 만들기 위해 우선 원격 저장소 페이지를 열어주세요.
그리고 상단 바의 Settings로 이동합니다.
그러면 화면 왼쪽에 메뉴가 나타납니다.
그 중 Security 섹션의 Secrets and variables - Actions 를 선택해주세요.
간혹 깃헙 페이지 UI가 업데이트되어 설정 화면의 메뉴 구성이 달라질 수 있습니다.
만약 위 이미지와 같은 메뉴 구성이 아니라면, Secret은 보안 관련 설정이기 때문에 Security 섹션을 살펴보거나, "Secret"과 "Actions" 키워드를 잘 찾아주세요!
2. Secret 생성하기
해당 메뉴로 이동하면 아래와 같은 화면이 나타납니다. 이 곳에서 Secret 변수를 만들고 관리할 수 있습니다.
Secrets 뿐 아니라 Variables 이라는 변수도 만들 수 있는데, Variable은 일반적인 텍스트나 그렇게 민감하지 않은 정보들을 담는 변수입니다. 워크플로에서 사용하는 값이 외부에 유출되어도 상관이 없다면 Variables를 사용해도 좋습니다.
지금 저희는 민감 정보를 다룰 것이니, Repository 레벨의 Secret을 만들기 위해 "New repository secret" 버튼을 클릭합니다.
아래와 같이 보이는 화면에서 Secret 이름과 데이터를 작성합니다.
단, 이름을 작성할 때는 아래 조건을 따라주어야 합니다.
- 허용되는 문자는 대문자, 소문자, 숫자, 언더바( _ )이며, 공백과 하이픈( - )은 쓸 수 없습니다.
- 숫자로 시작할 수 없습니다. 첫 글자는 영문 또는 언더바여야 합니다.
- "GITHUB_" 으로 시작할 수 없습니다. 이는 워크플로 내에서 예약어로 적용되어있기 때문입니다.
- 변수 이름이 모두 대문자로 자동 변환되어 저장되며, 참조 시에도 대소문자 구분을 하지 않습니다.
- Repository, Organization, Environment 내에서 고유한 이름이어야 합니다.
대부분의 커뮤니티에서 기능을 명확하고 간결하게 설명하는 대문자 snake_case를 사용하고 있으니 참고하여 작성하시면 되겠습니다.
- ex) AWS_ACCESS_KEY, GOOGLE_SERVICES_JSON 등
무엇보다 다른 팀원이 이해할 수 있고, 팀 내에서 합의된 컨벤션으로 정하는 것이 좋겠죠?
그리고 한번 설정한 이름은 변경할 수 없으니 이 또한 참고하시기 바랍니다.
Secret에 담길 데이터를 넣을 때는 필요한 데이터 원문을 그대로 입력해주면 됩니다.
지도 SDK를 사용하고 있다고 가정해봅시다. SDK를 사용하기 위해 제공받은 API Key 값이 로컬 파일에 아래와 같이 저장되어있다면,
MAPS_API_KEY=a1b2c3d4e5...
Secret 생성 시에도 동일하게 입력하여 저장하면 됩니다.
"Add secret"을 눌러 생성을 마치면, 목록에 Secret이 새롭게 추가된 것을 확인할 수 있습니다.
생성한 Secret은 수정/삭제가 가능합니다. 만약 Secret 값을 잘못 입력했거나 새로운 값으로 변경이 필요하다면 아이템 우측의 연필 아이콘을 눌러 수정할 수 있습니다.
다만 이전에 저장되었던 값은 불러올 수 없으니 참고해주세요.
3. Secret 적용하기
Secret을 만들었으니 이제 워크플로에 적용해봅시다.
워크플로에서는 아래와 같이 접근하여 secret 변수 값을 가져올 수 있습니다.
${{ secrets.MAPS_API_KEY }}
secrets 라는 Context를 통해 저희가 설정했던 secret 변수를 참조할 수 있습니다.
변수를 참조할 때는 대문자만으로 접근 가능하다는 것을 잊지 마세요!
name: Android CI
on:
pull_request:
branches: [ "develop" ]
jobs:
ci:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Check Lint
run: ./gradlew ktlintCheck
- name: Run Test
run: ./gradlew testDebugUnitTest --stacktrace
이전 포스팅에서 사용했던 CI 워크플로입니다.
만약 빌드에 필요한 파일 이름이 secrets.properties 라면(구글 맵 기준) 아래와 같이 파일을 생성할 수 있습니다.
name: Android CI
on:
pull_request:
branches: [ "develop" ]
jobs:
ci:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# secret 변수 MAPS_API_KEY의 값을 가져와 secrets.properties 파일 생성
- name: Create Maps API Key Properties
run : echo ${{ secrets.MAPS_API_KEY }} > secrets.properties
- name: Check Lint
run: ./gradlew ktlintCheck
- name: Run Test
run: ./gradlew testDebugUnitTest --stacktrace
이렇게 secrets에 접근하여 바로 사용할 수도 있으며, env 변수로 할당하여 사용할 수도 있습니다.
# 워크플로 내 전역 범위(top level) env
env:
FIRST_SECRET: ${{ secrets.FIRST_SECRET }}
jobs:
test-job:
runs-on: ubuntu-latest
# job 범위 env
env:
SECOND_SECRET: ${{ secrets.SECOND_SECRET }}
steps:
- name: "Get First Secrets From Workflow Level env"
run: echo $FIRST_SECRET > workflow_env_secret
- name: "Get Second Secrets From Job Level env"
run: echo $SECOND_SECRET > job_env_secret
- name: "Get Third Secrets From Step Level env"
run: echo $THIRD_SECRET > step_env_secret
# step 범위 env
env:
THIRD_SECRET: ${{ secrets.THIRD_SECRET }}
env: 는 쉽게 말해, 환경 변수 또는 정적 변수처럼 사용할 수 있는 값입니다. 어느 위치에서 선언하느냐에 따라 접근할 수 있는 범위가 달라집니다.
만약 워크플로 내 여러 개의 job에서 참조하는 Secret이라면 매번 secrets에 접근해서 꺼내 쓰기 보다는, 전역 범위에서 secret 값을 갖는 env를 생성하고, 이 env를 사용하는게 훨씬 편하고 읽기도 좋겠죠?
추가로, 만약 secrets를 echo와 같은 명령어로 워크플로 로그에 출력시키는 경우에는 GitHub에서 자동으로 마스킹 처리를 해줍니다.
secret이 외부에 함부로 유출될 일은 없으나, 로그에 출력시키는 등의 행위는 주의하는 것이 좋습니다.
효율적인 CI 워크플로 설계
Secret을 사용하는 방법을 알아보았으니, 이제 기초적인 CI 워크플로를 만들고 관리할 수 있습니다.
간단한 빌드와 린트 체크, 단위 테스트 만을 수행한다면 지금까지 배운 것만으로도 충분할 것입니다.
하지만 더 자세한 설정으로 워크플로를 더욱 효율적으로 개선할 수 있습니다.
Job 분리하기
Job 병렬 실행
빠르게 끝나는 작업으로 구성된 워크플로라면 모든 작업을 하나의 Job에서 수행해도 괜찮지만, 여러 동작이 추가된다면 워크플로가 오래 걸릴 수 있습니다. 이 경우 Job을 병렬적으로 실행하도록 설계할 수 있습니다.
Job은 Runner 단위로 개별적으로 실행되는 작업 모음입니다. 따라서 Job 별로 OS를 선택하여 다른 작업을 병렬적으로 수행할 수 있습니다. 만약 오래 걸리는 작업이 있는데, 이 작업이 다른 작업과 순차적인 관계에 있지 않다면 병렬적으로 실행하여 시간을 절약할 수 있습니다.
대표적인 예시로, CI 워크플로에서 UI 테스트를 실행하는 경우 별도의 Job으로 분리할 수 있습니다.
워크플로에서 UI 테스트를 실행하기 위해서는 별도의 에뮬레이터를 실행하는 Action이 필요합니다. 에뮬레이터를 세팅하고 실행하는데 대기하는 시간이 약 6~7분 정도로 상당히 오래 걸립니다.
만약 < 빌드 ➡️ 린트 체크 ➡️ 단위 테스트 > 이후에 UI 테스트까지 실행한다면, CI가 길게는 15분이 넘게 소모되는 끔찍한 광경을 겪을 수 있습니다.
CI에서 UI 테스트를 실행하는 방법은 다른 포스팅에서 자세히 다뤄보겠습니다.
이러한 경우 각 작업들을 별도의 Job으로 분리한다면 시간을 조금이나마 절약할 수 있습니다.
실제 스타카토에서는 아래 작업들을 각각의 Job으로 분리해 병렬적으로 실행시킴으로서, 워크플로의 실행 시간을 단축시키고 있습니다.
- 프로젝트 세팅(Secret 이용한 파일 생성 등)
- 빌드
- 린트 체크
- 단위 테스트
- UI 테스트
setup (프로젝트 세팅), ktlint-check (린트 체크), unit-tests (단위 테스트), ui-tests (UI 테스트)가 각각 Job으로 설정되어 있습니다.
...
# 각 작업들을 Job으로 나누어 병렬적으로 실행합니다.
jobs:
setup:
name: Setup for CI
runs-on: ubuntu-latest
...
ktlint-check:
name: Check Ktlint
runs-on: ubuntu-latest
needs: setup
...
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
needs: setup
...
ui-tests:
name: Run UI Test on [API ${{ matrix.api-level }} - ${{ matrix.profile }}]
runs-on: ubuntu-latest
needs: setup
strategy:
matrix:
include:
- api-level: 28
profile: "pixel_4"
- api-level: 32
profile: "pixel_6"
- api-level: 32
profile: "Nexus 7"
...
Job 순차 실행
그리고 Job을 병렬적으로 실행하더라도, 이전의 Job이 선행되어야만 하는 경우도 있습니다.
방금 전 워크플로를 다시 살펴볼까요?
...
jobs:
setup:
name: Setup for CI
runs-on: ubuntu-latest
...
ktlint-check:
name: Check Ktlint
runs-on: ubuntu-latest
needs: setup
...
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
needs: setup
...
ui-tests:
name: Run UI Test on [API ${{ matrix.api-level }} - ${{ matrix.profile }}]
runs-on: ubuntu-latest
needs: setup
strategy:
matrix:
include:
- api-level: 28
profile: "pixel_4"
- api-level: 32
profile: "pixel_6"
- api-level: 32
profile: "Nexus 7"
...
위 코드에서 자세히 다루지는 않았지만, setup Job에서는 워크플로에서 Secret 값을 가져와 필요한 파일(secrets.properties 등)을 생성하는 작업이 이루어집니다.
🧑💻👩💻(이 글을 읽으시는 똑똑한 독자분들) :
오잉? Job은 독립적인 Runner에서 실행된다고 했는데, 그럼 setup 에서 생성된 파일은 다른 Job에서는 생성되지 않는 것 아닌가요?
잘 짚으셨습니다! 각 Job은 독립적이기 때문에, setup Job에서 생성된 파일은 다른 Job에 존재하지 않습니다. 그래서 위 예시에서 나타나있지 않지만, setup 에서 만든 파일을 다른 Job에게 전달하는 동작을 하고 있습니다.
그리고 setup에서 파일을 생성하여 전달을 완료한 이후에 다른 Job이 실행되어야 하기 때문에 Job의 실행 순서를 제어했습니다.
needs: 를 사용하면 Job 간 실행 순서를 순차적으로 설정할 수 있습니다.
잘 보시면 setup 을 제외한 ktlint-check, unit-tests, ui-tests Job에 needs: 라는 키워드가 붙어있는 것을 확인할 수 있는데요. 해당 Job들이 모두 setup Job이 끝난 이후에 실행된다는 의미입니다. 그래서 이 워크플로는 setup 이 실행되어 필요한 파일을 전달한 뒤 3개의 Job이 병렬적으로 실행됩니다.
조건부 실행
워크플로에서 이벤트 필터와 if: 키워드를 사용해 워크플로 전체 또는 Job이나 Step이 조건에 따라 실행되도록 제어할 수 있습니다.
만약 코드가 잘못되어 빌드가 실패했는데, 이후에 린트 체크나 테스트 실행 등의 작업을 수행한다면 시간이 낭비되겠죠? 조건을 적절히 설정하면 실행하지 않아도 되는 워크플로나 Job, Step을 건너뛰어 시간을 절약할 수 있습니다.
GitHub Actions에서는 두 가지 레벨에서 조건부를 지원해줍니다.
- 이벤트 필터(on: 레벨) – 아예 워크플로를 기동하지 않거나 특정 파일/브랜치/태그에서만 기동
- 런타임 조건(if: 레벨) – 워크플로는 실행되지만, 특정 Job 또는 Step을 건너뛰기
이벤트 필터(on:)
이벤트 필터는 “언제 이 워크플로를 실행할까?”를 결정짓습니다. 즉, 어떤 이벤트에 따라 해당 워크플로를 트리거할지 제어할 수 있습니다.
워크플로에서 on: 키워드를 사용하여 이벤트 필터를 설정할 수 있으며, 이 키워드 다음에 설정할 수 있는 이벤트는 브랜치, 태그, 경로(파일) 3가지 종류에 따라 세분화됩니다.
해당 파트에서는 자주 사용되거나, 현재 스타카토에 적용된 필터를 위주로 간략하게 소개합니다.
브랜치 필터
특정 브랜치에 대한 이벤트를 워크플로 트리거로 지정할 수 있습니다.
- 특정 브랜치에 Push 했을 때
- 특정 브랜치로의 Pull Request가 열렸거나 닫혔을 때 등
만약 develop 브랜치에 대해 PR이 열렸을 때에만 현재 워크플로를 실행시키고 싶다면 아래와 같이 설정할 수 있습니다.
on:
pull_request:
branches:
- develop
develop에 대한 PR에 커밋이 추가될 때마다 해당 워크플로가 실행되기 때문에, CI 워크플로의 트리거를 설정할 때 유용한 방식입니다.
아래는 특정 브랜치에 대해 Push 동작이 일어났을 때 워크플로가 실행되는 이벤트 필터입니다.
on:
push:
branches:
- develop
- 'release/**' # release/ 로 시작하는 모든 브랜치
PR이 병합된 후 위 브랜치들에 대해 커밋이 추가되었을 때(Push와 동일) 해당 워크플로가 실행되기에, 개발 중인 기능을 바로 테스트하거나 서비스를 배포하는 CD 워크플로를 설정할 때 유용합니다.
태그 필터
특정 태그에 대해서도 이벤트를 추가할 수 있습니다.
on:
push:
tags:
- 'v2.**'
v2. 로 시작하는 태그가 push 되었을 때만 워크플로가 실행됩니다. 릴리스 빌드 트리거에 유용합니다.
경로 필터
특정 파일이나 디렉터리에 대한 경로 및 패턴을 지정하여, 해당 경로에서 변경사항이 생겼을 때만 워크플로가 실행됩니다.
on:
paths:
- '**.xml' # 모든 xml 파일의 변경사항
- 'android/presentation/**' # 안드로이드 presentation 패키지 내 변경사항
스타카토의 경우 develop 브랜치를 서버와 안드로이드가 함께 사용하고 있어, 잘못하면 타 분야의 CI 워크플로가 실행될 수 있습니다. 안드로이드 관련 작업만 한 후 PR을 만들었는데, 백엔드의 CI가 함께 실행된 적도 있었죠.
on:
paths:
- 'android/**'
pull_request:
branches:
- develop
그래서 위와 같이 경로 필터와 브랜치 필터를 함께 사용하여, 안드로이드 프로젝트에 변경 사항이 있는 경우에는 안드로이드 CI를, 백엔드 프로젝트에 변경 사항이 있으면 백엔드 CI를 실행시키도록 설정했습니다.
이 외에도 코드 리뷰, 이슈 등 다양한 이벤트를 감지할 수 있어, 상황에 따라 원하는 워크플로를 설정할 수 있습니다. 이벤트에 대한 더 자세한 정보는 공식 문서를 참고해주세요!
Events that trigger workflows - GitHub Docs
You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs.
docs.github.com
런타임 조건(if:)
이벤트 필터가 워크플로의 실행 자체를 제어했다면, 런타임 조건 if: 는 워크플로가 트리거된 후 특정 Job이나 Step의 실행을 제어합니다.
Job 레벨 if:
아래와 같이 Job 레벨에서 해당 Job의 실행 여부를 결정지을 수 있습니다. if 조건문은 Runner 실행 전에 먼저 체크되기 때문에, 조건을 잘 설정한다면 불필요한 Runner 실행을 방지해 자원과 시간을 절약할 수 있습니다.
jobs:
deploy:
if: startsWith(github.event.pull_request.head.ref, 'release/')
runs-on: ubuntu-latest
steps:
- run: ...
해당 조건문은 PR의 소스 브랜치가 ‘release/’로 시작하는 경우에만 deploy Job을 실행합니다.
참고로 스타카토에서는 아래와 같이 설정하여 release/an → main 브랜치에 대한 PR이 병합되었을 때 배포 작업을 시작하도록 설계했습니다.
# main 브랜치에 대한 PR이 닫혔을 때(closed)
on:
pull_request:
branches:
- main
types:
- closed
jobs:
build-and-deploy:
# Pull Request가 병합되고 소스 브랜치가 'release-an' 브랜치일 때
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release-an/')
...
Step 레벨 if:
Job 내에서 특정 Step의 실행 여부 또한 조건문으로 제어할 수 있습니다.
Step에서 사용하는 if: 는 GitHub 이벤트 뿐 아니라, Status Check Functions를 활용하여 현재 워크플로와 이전 Step의 상태를 가져올 수 있습니다.
기본적으로 Job 안의 Step은 이전 Step이 실패할 때 자동으로 건너뛰어집니다. 이전 Step의 실패 여부에 관계 없이 특정 Step을 항상 실행하거나, 실패 했을 경우에만 특정 Step을 실행시키도록 제어할 수 있습니다.
- 이전 Step이 성공한 경우 true를 반환하는 success()
steps:
...
- name: Run Test
...
# 이전 step이 모두 성공했을 때
- name: Test has succeeded
if: ${{ success() }}
...
- 이전 Step이 실패한 경우 true를 반환하는 failure()
steps:
...
- name: Run Test
...
# 이전 step이 하나라도 실패했을 때
- name: Emit the test failure
if: ${{ failure() }}
...
- 그리고 현재 워크플로의 취소를 감지하는 cancelled() 도 있습니다.
steps:
...
# 워크플로 취소 시 작업
- name: Clean up workflow
if: ${{ cancelled() }}
...
if: 에서는 기본적으로 표현식( ${{ }} )를 생략할 수 있습니다. 그러나 not 연산자( ! )를 사용하는 경우에는 표현식을 필수로 사용해야 합니다.
만약 특정 Step을 항상 실행시키고 싶다면 always()라는 상태 확인 함수를 사용할 수 있습니다.
다만 이전 Step의 성공 여부가 중요한 영향을 미치는 경우에는 always() 사용을 권장하지 않습니다. 워크플로 실행에 큰 영향을 끼치지 않는 작업인 경우에서만 always()를 활용하는 것이 좋습니다.
저는 Step 레벨에서의 if: 를 테스트 결과 출력에 사용하고 있습니다.
- name: Run Unit Test
run: ./gradlew testDebugUnitTest
- name: Publish Unit Test Results
if: always()
...
테스트를 실행한 후 실패 여부와 관계 없이 테스트 결과를 내보낼 때(결과 로그 출력, 파일 저장 또는 PR 코멘트 출력 등) 위와 같이 활용할 수 있습니다.
Matrix Strategy
Running variations of jobs in a workflow - GitHub Docs
A matrix strategy lets you use variables in a single job definition to automatically create multiple job runs that are based on the combinations of the variables. For example, you can use a matrix strategy to test your code in multiple versions of a langua
docs.github.com
간혹 여러 개의 동일한 작업을 수행하도록 설계해야할 수 있습니다. 그럴 때 Matrix Strategy를 활용하면 하나의 Job을 정의해서 여러 작업을 병렬로 실행시킬 수 있습니다.
Matrix Strategy란?
하나의 Job에서 여러 개의 환경을 실행하는 기능입니다.
matrix strategy는 변수 배열을 선언하고, 해당 변수들의 모든 조합만큼 Job 인스턴스를 생성해 줍니다. 같은 워크플로를 유지하면서 운영체제, 언어 버전, 디바이스 API 등을 동시에 테스트할 수 있습니다.
Matrix 사용 방법
변수를 하나 이상 선언하고, 그 옆에 변수에 할당될 값을 배열에 정의합니다. 변수에 들어갈 수 있는 값은 정수, 문자열 등이 있습니다.
jobs:
matrix-example:
strategy:
matrix:
cores: [2, 4, 8]
os: [ubuntu_latest, macos-latest]
위 matrix 사용 예제에서는 아래와 같이 6개의 조합으로 Job이 각각 실행됩니다.
- {cores: 2, os: ubuntu-latest}
- {cores: 2, os: windows-latest}
- {cores: 4, os: ubuntu-latest}
- {cores: 4, os: windows-latest}
- {cores: 8, os: ubuntu-latest}
- {cores: 8, os: windows-latest}
matrix 변수를 사용할 때는 아래와 같이 “matrix” Context를 통해 접근합니다.
jobs:
test:
runs-on: ${{ matrix.os }} # 변수 사용
만약 matrix 에서 기존 조합을 변경 또는 확장하고 싶거나 새로 추가하려는 조합이 있다면 include: 를 활용할 수 있습니다.
jobs:
matrix-example:
strategy:
matrix:
cores: [2, 4, 8]
os: [ubuntu-latest, macos-latest]
include:
- cores: 2
ram: 8GB
- cores: 8
os: ubuntu-latest
- cores: 16
os: windows-latest
- “cores: 2”에 해당하는 조합은 “ram: 8GB” 변수가 확장됩니다.(기존 조합에서 확장)
- {cores: 2, os: ubuntu-latest, ram: 8GB}
- {cores: 2, os: windows-latest, ram: 8GB}
- “cores: 8”을 포함하는 조합은 os가 ubuntu-latest로 변경됩니다.(기존 조합에서 변경)
- {cores: 8, os: ubuntu-latest}
- {cores: 8, os: ubuntu-latest}
- “cores: 16”을 포함하는 기존 조합은 없으므로, 해당 조합이 새롭게 추가됩니다.(새 조합 추가)
- {cores: 16, os: windows-latest}
특정 조합을 제외하고 싶다면 exclude: 를 사용할 수 있습니다.
jobs:
matrix-example:
strategy:
matrix:
cores: [2, 4, 8]
os: [ubuntu-latest, macos-latest]
exclude:
- cores: 2
os: macos-latest
- “cores: 2, os: macos-latest”를 포함하는 조합을 제외하고 총 5개의 Job이 실행됩니다.
안드로이드 UI Test를 CI에서 실행하는 경우, matrix를 적용하여 여러 API 버전과 기기 종류에서 테스트가 수행되도록 설계할 수 있습니다.
jobs:
ui-test:
runs-on: ubuntu-latest
strategy:
matrix:
api-level: [26, 29, 34] # 26, 29, 34 버전
device: ['pixel_6', 'Nexus 7'] # Pixel 6, Nexus 7 기기
steps:
- uses: actions/checkout@v4
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
profile: ${{ matrix.device }}
- run: ./gradlew connectedCheck
CI에서 사용하면 편리한 Actions
CI 워크플로를 다루면서 저와 팀의 입맛에 맞추어 조금씩 편하게 개선해나갔습니다. 그 과정에서 CI 워크플로에서 유용한 Action을 몇 가지 알게되어, 이 글에서 가볍게 소개해볼까 합니다.
유용한 Action을 발견하면 주기적으로 업데이트할 예정입니다!
actions/upload‑artifact & actions/download‑artifact (v4)
https://github.com/actions/upload-artifact
GitHub - actions/upload-artifact
Contribute to actions/upload-artifact development by creating an account on GitHub.
github.com
https://github.com/actions/download-artifact
GitHub - actions/download-artifact
Contribute to actions/download-artifact development by creating an account on GitHub.
github.com
Github Actions에서는 Artifact라는 것을 지원해줍니다. 쉽게 설명하자면 무언가를 보관할 수 있는 책상의 서랍장 같은 것입니다. 워크플로라는 작업대에서 작업을 수행하고, 작업대를 깨끗이 치우기 전 필요한 것들을 서랍에 보관하는 것과 비슷합니다.
빌드 결과물(테스트 리포트, APK, Coverage 등)을 Job → Job 간에 넘기거나, 워크플로가 종료된 뒤에도 파일을 저장해두는 표준 Action입니다.
Runner가 달라도 동일 워크플로 안에서 자유롭게 업로드, 다운로드를 할 수 있으며, 최대 10 GB까지 저장할 수 있습니다.
v1–v3 버전은 2024년 11월 30일부로 deprecated 되었습니다. 새로운 버전인 v4 를 사용하세요!
저는 Artifact를 이용해 빌드 파일(APK, AAB) 또는 테스트 결과를 업로드하여 따로 확인하거나, 워크플로 안의 다른 Job에 필요한 파일을 넘겨주고 있습니다. 유용하게 확인할 수 있는 Artifact를 어떻게 사용하는지 살펴봅시다.
사용 방법
Artifact에 파일을 업로드하는 방법입니다.
steps:
...
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Upload APK to artifact
uses: actions/upload-artifact@v4
with:
name: app-debug.apk # Artifact에 저장할 파일 이름
path: .../app/build/outputs/apk/debug/app-debug.apk # Artifact에 저장할 파일 경로
overwrite: true # 덮어쓰기 옵션 설정
...
with: 키워드 아래에서 파일 이름, 파일 경로 등을 설정할 수 있습니다.
- name (선택): 업로드할 파일의 이름을 설정합니다. 따로 설정하지 않으면 “artifact”라는 이름으로 업로드됩니다.
- path (필수): 업로드할 파일이 저장된 경로입니다. 올바른 경로를 작성하지 않으면 아무런 파일을 업로드할 수 없으므로 꼼꼼히 확인하세요!
- overwrite (선택): 이미 동일한 이름의 파일이 있는 경우 현재 파일로 덮어쓸 지 여부를 설정합니다. 기본으로 false로 설정되어 있습니다.
그 외 다양한 옵션이 있으므로, 자세한 내용은 upload-artifact GitHub Repository에서 확인해주세요!
그리고 Artifact로부터 파일을 다운로드하는 방법은 아래와 같습니다.
steps:
...
- name: Download Config Files
uses: actions/download-artifact@v4
with:
name: config-files # Artifact에 저장된 파일 이름
path: ... # Artifact의 파일을 저장할 경로 지정
...
마찬가지로 with: 키워드 아래에서 몇 가지 옵션을 설정할 수 있습니다.
- name (선택): Artifact에 업로드 된 파일의 이름을 지정합니다. 따로 설정하지 않으면 Artifact 내 모든 파일을 가져옵니다.
- path (선택): 업로드된 파일을 현재 워크플로에 저장할 경로입니다. 따로 설정하지 않으면 워크플로 내 기본 경로인 $GITHUB_WORKSPACE 에 저장됩니다.
이 외에도 다양한 다운로드 옵션이 있으므로, 자세한 정보는 download-artifact GitHub Repository에서 확인해주세요!
이렇게 빌드 파일을 업로드하는 것 외에도, 워크플로 내 다른 Job에게 빌드에 필요한 설정 파일을 전달하는데 사용할 수 있습니다.
jobs:
setup:
name: Setup for CI
...
# 필요한 설정 파일을 Artifact에 업로드
- name: Upload Config Files
uses: actions/upload-artifact@v4
with:
name: config-files
path: |
android/Staccato_AN/local.properties
android/Staccato_AN/secrets.properties
android/Staccato_AN/local.defaults.properties
android/Staccato_AN/app/google-services.json
retention-days: 15
overwrite: 'true'
...
unit-tests:
name: Run Unit Tests
...
# 필요한 설정 파일을 Artifact에서 다운로드
- name: Download Config Files
uses: actions/download-artifact@v4
with:
name: config-files
path: ./android/Staccato_AN
EnricoMi/publish‑unit‑test‑result‑action (v2)
https://github.com/EnricoMi/publish-unit-test-result-action
GitHub - EnricoMi/publish-unit-test-result-action: GitHub Action to publish unit test results on GitHub
GitHub Action to publish unit test results on GitHub - EnricoMi/publish-unit-test-result-action
github.com
JUnit, NUnit, Mocha 등 다양한 테스트 결과 파일(XML/TRX/JSON)을 분석해 PR에 코멘트로 요약을 남겨 주는 액션입니다.
사용 방법도 아주 간단합니다. 아래는 기본적인 사용 예시입니다.
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: ${{ !cancelled() }} # 워크플로 중단 시 건너뜀
with:
files: '**/test-results/**/*.xml'
단, Job이 PR에서 코멘트를 추가할 수 있도록 쓰기 권한을 반드시 부여해주어야 합니다.
jobs:
run-test:
# write 권한 설정 필요
permissions:
checks: write
pull-requests: write
private 레포지토리에서 사용한다면 아래와 같이 권한을 설정해주세요.
permissions:
contents: read
issues: read
checks: write
pull-requests: write
실행이 끝나면 아래와 같이 테스트 통과/실패 통계, ⏱ 소요 시간, ⛔️ 에러 목록이 자동으로 PR 코멘트로 달려 리뷰 효율을 높여 줍니다.
옵션에서 출력 형식을 변경할 수도 있으므로, 해당 Action의 GitHub Repository를 방문해보시기 바랍니다.
마치며
지금까지 워크플로에서 Secret을 사용하는 방법과 효율적인 워크플로를 구성하는 방법, CI 워크플로에서 쓰기 유용한 Action들을 알아보았습니다.
전하고 싶은 내용이 많아서 글이 길어졌네요.
처음 CI/CD를 구축했을 때 낯선 개념과 정보 투성이었기에 여러 난관을 겪었습니다. 다른 분들은 저처럼 헤매지 않고 쓸만한 워크플로를 설계할 수 있었으면 하는 마음에서 GitHub Actions 시리즈를 시작하게 되었습니다.
최대한 이해하기 쉽고 금방 적용할 수 있도록 자세하게 적어보았습니다만, 혹시나 틀린 정보가 있거나 좀 더 자세한 설명이 필요하다면 언제든 댓글에 남겨주세요!
GitHub Actions 시리즈의 다음 포스팅은 QA 및 앱 테스트를 위한 CD 워크플로를 설계하는 방법을 설명해보고자 합니다.
참고 자료
- https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/using-secrets-in-github-actions
- https://docs.github.com/en/actions/reference/events-that-trigger-workflows
- https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
- https://github.com/actions/upload-artifact
- https://github.com/actions/download-artifact
- https://github.com/EnricoMi/publish-unit-test-result-action