← 블로그 목록

자동화 파이프라인 완성 — 업데이트부터 Mattermost 알림까지

2026-09-05
JenkinsAllureMattermostCI/CD자동화

자동화 테스트는 만들어놓는다고 끝이 아니다. 매일 최신 버전 서버에서 돌아야 하고, 결과를 리포트로 남겨야 하고, 팀이 결과를 바로 알 수 있어야 한다.

이걸 Jenkins 파이프라인 세 단계로 연결했다.

[Jenkins 스케줄]
       │
       ▼
[1단계] 서버 자동 업데이트
  NAS 마운트 → 최신 패키지 선택 → 설치 → 서비스 체크
       │
       ▼
[2단계] 테스트 실행 + Allure 전송
  npm ci → bddgen → playwright test
  → allure-results 압축 → Allure 서버 전송
       │
       ▼
[3단계] Mattermost 알림
  JUnit XML 파싱 + Allure JSON 파싱
  → 성공/실패 요약 → MM 채널 전송

1단계: NAS에서 최신 패키지 자동 설치

매일 빌드 서버에서 새 패키지가 NAS에 올라온다. 이 스크립트가 그 패키지를 자동으로 찾아서 설치한다.

#!/bin/bash
set -x

# 히스토리 초기화
cd /opt/history || exit 1
rm -rf *
sleep 5

# 최신 패키지 선택
LATEST_FILE=$(ls -t /mnt/nas/package/security_allinone/security_allinone_*.BIN 2>/dev/null | head -1)

if [ -z "$LATEST_FILE" ]; then
  echo "[ERROR] No package found"
  exit 1
fi

DEST_FILE=/opt/tmp/$(basename "$LATEST_FILE")

# 기존 파일 정리 후 복사
rm -f /opt/tmp/security_allinone_* 2>/dev/null
sudo cp "$LATEST_FILE" "$DEST_FILE"
sudo chmod +x "$DEST_FILE"

# 설치 실행 (인수 2 = 자동 설치 모드)
"$DEST_FILE" 2

# 서비스 기동 대기
sleep 60

# 서비스 상태 확인
ps -ef | grep tomcat | grep -v grep && echo "[OK] Tomcat running" || echo "[WARN] Tomcat not detected"
ps -ef | grep httpd | grep -v grep && echo "[OK] HTTPD running" || echo "[WARN] HTTPD not detected"

포인트는 두 가지다.

ls -t ... | head -1: NAS에 쌓인 패키지 중 가장 최신 파일을 자동으로 선택한다. 파일명에 날짜나 버전이 포함되어 있어서 ls -t로 수정 시간 역순 정렬하면 된다.

설치 후 60초 대기: 패키지 설치가 완료돼도 서비스가 완전히 뜨기까지 시간이 걸린다. Tomcat과 HTTPD 기동을 확인하고 넘어간다.

2단계: 테스트 실행 + Allure 서버 전송

#!/bin/bash

TEST_RESULT=0

# 서버 기동 대기 옵션 (필요 시 활성화)
if [ "$STARTING_PING" = "true" ]; then
    echo "Waiting for server startup..."
    sleep 600
fi

# 의존성 설치
npm ci
npx playwright install --with-deps

# BDD 코드 생성
npx bddgen --verbose

# 테스트 실행 — 실패해도 중단하지 않고 결과값만 저장
npx playwright test --project=product-a --project=product-b --workers=1
TEST_RESULT=$?

여기서 핵심은 TEST_RESULT=$?다. 테스트가 실패해도 스크립트가 중단되지 않는다. Allure 리포트를 만들고 전송하는 과정을 다 마친 뒤, 맨 마지막에 exit $TEST_RESULT로 Jenkins에 빌드 성공/실패를 전달한다. 이걸 빠뜨리면 테스트가 실패했는데 Jenkins 빌드는 성공으로 표시되는 상황이 생긴다.

# Allure 프로젝트 ID 생성 (JOB_NAME 기반)
PROJECT_NAME_RAW=$(basename "$JOB_NAME")
PROJECT_NAME_FINAL=$(echo "$PROJECT_NAME_RAW" | tr '[:upper:]' '[:lower:]' | sed -e 's/_/-/g' -e 's/\./-/g')
export PROJECT_NAME="${PROJECT_NAME_FINAL}-edr"

echo "Final Allure Project ID: $PROJECT_NAME"

# Allure 프로젝트 생성 (없으면 생성, 있으면 패스)
curl -X POST "${ALLURE_SERVER_URL}/projects" \
     -H "Content-Type: application/json" \
     -d "{\"id\": \"${PROJECT_NAME}\", \"name\": \"${PROJECT_NAME} Report\"}"

# allure-results 압축
rm -f allure-results.zip
(cd allure-results && zip -r "../allure-results.zip" .)

# 개별 프로젝트 + 통합 대시보드 두 군데 전송
curl -X POST "${ALLURE_SERVER_URL}/send-results?project_id=${PROJECT_NAME}" \
     -H "Content-Type: multipart/form-data" \
     -F "files[]=@allure-results.zip;type=application/zip"

curl -X POST "${ALLURE_SERVER_URL}/send-results?project_id=0-total-dashboard" \
     -H "Content-Type: multipart/form-data" \
     -F "files[]=@allure-results.zip;type=application/zip"

exit $TEST_RESULT

Allure 결과를 두 군데에 전송하는 게 포인트다. 개별 프로젝트 리포트와 0-total-dashboard라는 통합 대시보드에 동시에 올린다. 팀원들은 각 제품 리포트에서 세부 내용을 보거나, 통합 대시보드에서 전체 현황을 한눈에 볼 수 있다.

프로젝트 ID는 JOB_NAME에서 자동으로 만든다. 대문자를 소문자로, _.-로 바꾸고 접미사를 붙인다. Jenkins Job 이름이 바뀌어도 규칙대로 따라간다.

3단계: Mattermost 알림 (Python)

테스트가 끝나면 Python 스크립트를 실행해서 결과를 채널에 알린다.

import os, requests, json, urllib3, sys
import xml.etree.ElementTree as ET
import urllib.parse

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_junit_summary():
    xml_path = "./test-results/junit.xml"
    if not os.path.exists(xml_path):
        return 0, 0
    try:
        tree = ET.parse(xml_path)
        total, failures = 0, 0
        for suite in tree.iter('testsuite'):
            total += int(suite.attrib.get('tests', 0))
            failures += int(suite.attrib.get('failures', 0))
        return total, failures
    except:
        return 0, 0

def send_notification(job_name, build_number, build_url):
    allure_results_path = "./allure-results"
    allure_report_url = f"{os.environ.get('ALLURE_SERVER_URL')}/projects/{os.environ.get('PROJECT_NAME')}/reports/latest"

    total_tc, fail_count = get_junit_summary()
    success_count = total_tc - fail_count

    # Allure result JSON에서 실패 테스트 이름 추출
    failed_tests = {}
    if os.path.exists(allure_results_path):
        for filename in os.listdir(allure_results_path):
            if filename.endswith("-result.json"):
                with open(os.path.join(allure_results_path, filename), 'r', encoding='utf-8') as f:
                    try:
                        data = json.load(f)
                        # retry=True인 재시도 케이스는 제외
                        if data.get('status') == 'failed' and not data.get('retry', False):
                            failed_tests[data.get('name')] = True
                    except:
                        continue

    if fail_count > 0:
        failures = [
            f"{idx}. {name}\n   [실패 원인 확인하기]({allure_report_url}?q={urllib.parse.quote(name)})"
            for idx, name in enumerate(failed_tests.keys(), 1)
        ]
        message = (
            f"**[빌드 결과] {job_name} - #{build_number}**\n\n"
            f"테스트 요약: 총 {total_tc}건 중 {success_count}건 성공, {fail_count}건 실패\n\n"
            f"**[실패 테스트 목록]**\n" + "\n".join(failures) + "\n\n"
            f"--------------------------------------------------\n"
            f"[전체 리포트 확인하기]({allure_report_url})\n"
            f"[Jenkins 콘솔 보기]({build_url})"
        )
    else:
        message = (
            f"**[빌드 결과] {job_name} - #{build_number}**\n\n"
            f"테스트 결과: 총 {total_tc}건 모두 성공! ✅\n\n"
            f"[전체 리포트 확인하기]({allure_report_url})\n"
            f"[Jenkins 콘솔 보기]({build_url})"
        )

    url = f"{os.environ.get('MM_SERVER_URL')}/api/v4/posts"
    payload = {
        "channel_id": os.environ.get('MM_CHANNEL_ID'),
        "message": message
    }
    headers = {
        "Authorization": f"Bearer {os.environ.get('MM_TOKEN')}",
        "Content-Type": "application/json"
    }
    requests.post(url, headers=headers, data=json.dumps(payload), verify=False)

if __name__ == "__main__":
    send_notification(
        sys.argv[1] if len(sys.argv) > 1 else "Unknown",
        sys.argv[2] if len(sys.argv) > 2 else "0",
        sys.argv[3] if len(sys.argv) > 3 else "#"
    )

이 스크립트는 두 가지 소스를 조합한다.

JUnit XML: test-results/junit.xml에서 전체 TC 수와 실패 수를 집계한다. Playwright가 junit reporter로 자동 생성해주는 파일이다.

Allure result JSON: allure-results/ 안의 *-result.json 파일을 하나씩 읽어서 status: failed인 케이스의 이름을 꺼낸다. 여기서 retry: true를 체크하는 게 중요하다. Playwright의 retries 설정으로 재시도한 케이스는 JSON에 retry: true로 표시되는데, 이걸 포함하면 같은 테스트가 중복으로 실패 목록에 들어간다.

실패 메시지에는 각 테스트 이름을 URL 인코딩해서 Allure 리포트 링크를 직접 달아준다. 팀원이 채널에서 링크를 누르면 해당 케이스의 실패 원인으로 바로 이동한다.

[빌드 결과] EDR_7.0 - #142

테스트 요약: 총 87건 중 84건 성공, 3건 실패

[실패 테스트 목록]
1. 동적 분석 탐지 결과 검색
   [실패 원인 확인하기](https://allure.../index.html?q=...)
2. 정책 생성 후 에이전트 적용 확인
   [실패 원인 확인하기](https://allure.../index.html?q=...)

--------------------------------------------------
[전체 리포트 확인하기](https://allure.../index.html)
[Jenkins 콘솔 보기](http://jenkins.../console)

Jenkins에서 실행하는 방법

Jenkinsfile 또는 Job 설정에서 순서대로 실행한다.

// Jenkinsfile 예시
pipeline {
    triggers {
        cron('0 22 * * *')  // 매일 밤 10시
    }
    stages {
        stage('Update Server') {
            steps {
                sh './scripts/update_server.sh'
            }
        }
        stage('Run Tests') {
            steps {
                sh './scripts/run_tests.sh'
            }
        }
    }
    post {
        always {
            sh """
                python3 ./scripts/notify_mm.py \
                    "${env.JOB_NAME}" \
                    "${env.BUILD_NUMBER}" \
                    "${env.BUILD_URL}"
            """
        }
    }
}

post { always { ... } }에 MM 알림을 넣는 게 핵심이다. 테스트가 성공하든 실패하든 항상 실행되고, sys.argv로 JOB_NAME, BUILD_NUMBER, BUILD_URL을 넘긴다.

TOKEN, CHANNEL_ID 같은 민감한 값은 코드에 직접 넣지 않고 Jenkins Credentials 또는 환경 변수로 관리한다.

전체 파이프라인이 완성되면

매일 밤 Jenkins가 스케줄로 실행된다. 서버에 최신 패키지가 설치되고, 테스트가 돌아가고, Allure 리포트가 생성되고, 결과 요약이 팀 채널에 올라온다. 아침에 출근하면 채널에 어젯밤 결과가 와 있다.

실패 케이스가 있으면 링크 하나로 바로 들어가서 스크린샷과 Trace를 확인할 수 있다. "어제 뭐가 터졌어요?" 물어볼 필요가 없어진다.