[TeamCity] Slack์—์„œ ๋นŒ๋“œ ๋ช…๋ น์–ด๋กœ TeamCity ๋นŒ๋“œํ•˜๊ธฐ - (2)

2025. 3. 7. 15:06

 

์ด์ „ ๋‹จ๊ณ„

 

1) TeamCity API ์„ค์ •

๐Ÿ“Œ TeamCity์—์„œ API ํ† ํฐ ์„ค์ •

 

1. TeamCity ์›นํŽ˜์ด์ง€ -> Profile -> Access Tokens ์ด๋™

2. Generate New Token ํด๋ฆญ -> teamcity_token ์ €์žฅ

 

 

๐Ÿ“Œ TeamCity์—์„œ API  ํ…Œ์ŠคํŠธ

API๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์˜ ๋ช…๋ น์–ด ์‹คํ–‰ํ•˜๊ธฐ

curl -X GET "http://{TeamCity_IP}:{Port}/app/rest/buildTypes" \
   -H "Accept: application/json" \
   -H "Authorization: Bearer {Your_Token}"

 

์ •์ƒ์ ์ธ JSON ์‘๋‹ต์ด ์˜ค๋ฉด TeamCity API ๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. โŒฏแต”โฉŠแต”โŒฏเฒฃ

 

2) Python Slack Bot ์ฝ”๋“œ ์ž‘์„ฑ

๐Ÿ“Œ slack_bot.py (slack์—์„œ ๋นŒ๋“œ! ์ž…๋ ฅ ์‹œ TeamCity ์‹คํ–‰)

import requests
import time
import threading
import xml.etree.ElementTree as ET
from flask import Flask, request, jsonify

app = Flask(__name__)

# ๐Ÿ”ฅ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • (์Šฌ๋ž™ & TeamCity)
SLACK_BOT_TOKEN = "xoxb-..."  # Slack Bot Token
TEAMCITY_URL = "http://192.168.50.46:80"  # TeamCity ์„œ๋ฒ„ ์ฃผ์†Œ
TEAMCITY_TOKEN = "eyJ0eXAiOiAiVENWMiJ9..."  # TeamCity API ํ† ํฐ
TEAMCITY_BUILD_ID = "Unity_Project_Build"  # ์‹คํ–‰ํ•  TeamCity ๋นŒ๋“œ ID

processed_events = set()  # ์ตœ๊ทผ ์ฒ˜๋ฆฌํ•œ ์ด๋ฒคํŠธ ์ €์žฅ

def trigger_teamcity_build(branch="main"):
    """TeamCity ๋นŒ๋“œ ์‹คํ–‰ ํ›„ ๋นŒ๋“œ ID ๋ฐ˜ํ™˜"""
    url = f"{TEAMCITY_URL}/app/rest/buildQueue"
    headers = {
        "Authorization": f"Bearer {TEAMCITY_TOKEN}",
        "Content-Type": "application/json",
        "Accept": "application/xml"  # XML ์‘๋‹ต์„ ๋ฐ›๋„๋ก ์„ค์ •
    }
    data = {
        "buildType": {"id": TEAMCITY_BUILD_ID},
        "properties": {"property": [{"name": "branch", "value": branch}]}
    }

    response = requests.post(url, headers=headers, json=data)
    
    print(f"๐Ÿ“ก TeamCity ๋นŒ๋“œ ์‹คํ–‰ ์š”์ฒญ ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
    print(f"๐Ÿ“ก TeamCity ์‘๋‹ต ๋‚ด์šฉ:\n{response.text}")  # XML ์‘๋‹ต์„ ์ถœ๋ ฅํ•˜์—ฌ ํ™•์ธ

    if response.status_code == 200:
        try:
            root = ET.fromstring(response.text)  # XML ํŒŒ์‹ฑ
            build_id = root.get("id")  # ๋นŒ๋“œ ID ์ถ”์ถœ
            print(f"๐Ÿš€ TeamCity ๋นŒ๋“œ ์‹คํ–‰ ์™„๋ฃŒ! ๋นŒ๋“œ ID: {build_id}")
            return build_id
        except ET.ParseError as e:
            print(f"โŒ XML ํŒŒ์‹ฑ ์‹คํŒจ! ์˜ค๋ฅ˜: {e}")
            return None
    else:
        print(f"โŒ TeamCity ๋นŒ๋“œ ์‹คํ–‰ ์‹คํŒจ! ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
        return None


def wait_for_build_completion(build_id, channel):
    """TeamCity ๋นŒ๋“œ ์™„๋ฃŒ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ Slack์— ์•Œ๋ฆผ ์ „์†ก"""
    url = f"{TEAMCITY_URL}/app/rest/builds/id:{build_id}"
    headers = {
        "Authorization": f"Bearer {TEAMCITY_TOKEN}",
        "Accept": "application/xml"  # XML ์‘๋‹ต์„ ๋ฐ›๋„๋ก ์„ค์ •
    }

    print(f"โณ ๋นŒ๋“œ ์™„๋ฃŒ ๋Œ€๊ธฐ ์ค‘... (๋นŒ๋“œ ID: {build_id})")

    while True:
        response = requests.get(url, headers=headers)
        print(f"๐Ÿ“ก TeamCity ๋นŒ๋“œ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {response.status_code}")

        if response.status_code == 200:
            try:
                root = ET.fromstring(response.text)  # XML ํŒŒ์‹ฑ
                state = root.get("state")  # ๋นŒ๋“œ ์ง„ํ–‰ ์ƒํƒœ
                status = root.get("status")  # ๋นŒ๋“œ ์„ฑ๊ณต/์‹คํŒจ ์ƒํƒœ

                print(f"๐Ÿ“Š TeamCity ๋นŒ๋“œ ์ƒํƒœ: state={state}, status={status}")

                if state == "finished":
                    result_text = "โœ… ๋นŒ๋“œ ์™„๋ฃŒ!" if status == "SUCCESS" else "โŒ ๋นŒ๋“œ ์‹คํŒจ!"
                    send_slack_message(channel, result_text)  # Slack ๋ฉ”์‹œ์ง€ ์ „์†ก
                    print(f"๐ŸŽ‰ {result_text} (๋นŒ๋“œ ID: {build_id})")
                    break
            except ET.ParseError as e:
                print(f"โŒ XML ํŒŒ์‹ฑ ์‹คํŒจ! ์˜ค๋ฅ˜: {e}")

        else:
            print(f"โŒ TeamCity ๋นŒ๋“œ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ! ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}, ์‘๋‹ต ๋‚ด์šฉ:\n{response.text}")

        time.sleep(10)  # 10์ดˆ๋งˆ๋‹ค ํ™•์ธ


def send_slack_message(channel, text):
    """Slack ์ฑ„๋„์— ๋ฉ”์‹œ์ง€ ์ „์†ก"""
    url = "https://slack.com/api/chat.postMessage"
    headers = {
        "Authorization": f"Bearer {SLACK_BOT_TOKEN}",
        "Content-Type": "application/json"
    }
    data = {
        "channel": channel,
        "text": text
    }

    response = requests.post(url, headers=headers, json=data)
    print(f"๐Ÿ“ค Slack ๋ฉ”์‹œ์ง€ ์ „์†ก ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
    print(f"๐Ÿ“ค Slack API ์‘๋‹ต ๋‚ด์šฉ: {response.text}")  # **Slack ์‘๋‹ต ๋ฐ์ดํ„ฐ ํ™•์ธ**

    if response.status_code != 200:
        print(f"โŒ Slack ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹คํŒจ! ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")



@app.route("/slack", methods=["POST"])
def slack_events():
    """Slack์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ """
    headers = dict(request.headers)
    if "X-Slack-Retry-Num" in headers:
        print(f"โš ๏ธ Slack Retry ์š”์ฒญ ๊ฐ์ง€ (ํšŸ์ˆ˜: {headers['X-Slack-Retry-Num']}) - ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€")
        return jsonify({"status": "ok"}), 200

    raw_data = request.get_data(as_text=True)
    print(f"\n๐Ÿ“ฉ Raw Slack Request:\n{raw_data}\n")

    try:
        data = request.json
        print(f"\n๐Ÿ“ฉ Parsed JSON:\n{data}\n")
    except Exception as e:
        print(f"\nโŒ JSON ๋ณ€ํ™˜ ์‹คํŒจ: {e}\n")
        return jsonify({"error": "Invalid JSON"}), 400

    if "challenge" in data:
        return jsonify({"challenge": data["challenge"]})

    if "event" in data:
        event = data["event"]
        event_id = event.get("client_msg_id") or event.get("event_id")
        
        if event_id in processed_events:
            print(f"โš ๏ธ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์ด๋ฒคํŠธ (ID: {event_id}) - ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€")
            return jsonify({"status": "ok"}), 200
        
        processed_events.add(event_id)

        if "text" in event and "!๋นŒ๋“œ" in event["text"]:
            print("\n๐Ÿš€ `!๋นŒ๋“œ` ๋ช…๋ น ๊ฐ์ง€ - TeamCity ๋นŒ๋“œ ์‹คํ–‰\n")
            channel = event["channel"]
            send_slack_message(channel, "๋นŒ๋“œ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿš€")

            # โœ… ๋นŒ๋“œ ์‹คํ–‰ ํ›„ ID ๋ฐ˜ํ™˜
            build_id = trigger_teamcity_build()
            if build_id:
                print(f"๐Ÿ”„ ๋นŒ๋“œ ์™„๋ฃŒ ๋Œ€๊ธฐ ์‹œ์ž‘ (๋นŒ๋“œ ID: {build_id})")
                
                build_thread = threading.Thread(
                    target=wait_for_build_completion,
                    args=(build_id, channel),
                    daemon=True  # ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ ์‹œ ์Šค๋ ˆ๋“œ๋„ ์ž๋™ ์ข…๋ฃŒ๋˜๋„๋ก ์„ค์ •
                )
                build_thread.start()

    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

 

์—ฌ๊ธฐ์„œ ๋‚ด teamcity ์„œ๋ฒ„์˜ ์ฃผ์†Œ(TEAMCITY_URL)๋Š” TeamCity๊ฐ€ ์‹คํ–‰์ค‘์ธ ์ปดํ“จํ„ฐ์˜ IP ์ฃผ์†Œ + ํฌํŠธ ๋ฒˆํ˜ธ์ด๋‹ค.

Mac ๊ธฐ์ค€ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํ„ฐ๋ฏธ๋„์— ๋‹ค์Œ์„ ์ž…๋ ฅํ–ˆ์„ ๋•Œ 

ifconfig | grep "inet "

 

inet 127.0.0.1 netmask 0xff000000
inet 192.168.0.20 netmask 0xffffff00 broadcast 192.168.0.255

์ด ์ค‘ 192.168.x.x ๋˜๋Š” 10.x.x.x ํ˜•์‹์˜ IP ์ฃผ์†Œ๊ฐ€ TeamCity ์„œ๋ฒ„ ์ฃผ์†Œ์ด๋‹ค.

์ด ์˜ˆ์‹œ์—์„œ๋Š” 192.168.0.20์ด TeamCity ์„œ๋ฒ„ ์ฃผ์†Œ๊ฐ€ ๋˜๋Š” ๊ฒƒ! (TEAMCITY_URL = "http://192.168.0.20:8111")

 

3)  ์‹คํ–‰ ๋ฐฉ๋ฒ•

๐Ÿ“Œ python ํŒจํ‚ค์ง€ ์„ค์น˜

pip install flask requests

 

๐Ÿ“Œ ์Šฌ๋ž™ API ์„œ๋ฒ„ ์‹คํ–‰

python slack_bot.py

 

๐Ÿ“Œ ngrok ์„ ์ด์šฉํ•ด ์™ธ๋ถ€์—์„œ ์ ‘์† ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ

1. ngrok ์„ ์„ค์น˜ํ•ด์ค€๋‹ค.

brew install ngrok

 

2. ngrok ํ™ˆํŽ˜์ด์ง€์—์„œ ๊ณ„์ • ์ƒ์„ฑ: https://ngrok.com/

 

ngrok | API Gateway, Kubernetes Networking + Secure Tunnels

ngrok simplifies app delivery by unifying API gateway, Kubernetes Ingress, global load balancing, DDoS protection and more with secure tunnels.

ngrok.com

 

3. ๋กœ๊ทธ์ธ ํ›„ Auth Token์„ ํ™•์ธ

4. ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด ์‹คํ–‰ (๋ฐœ๊ธ‰๋œ ํ† ํฐ ์‚ฌ์šฉ)

ngrok config add-authtoken YOUR_AUTH_TOKEN

 

5. ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

cat ~/.ngrok2/ngrok.yml

 

6. ngrok ์‹คํ–‰ ๋ฐ ํ…Œ์ŠคํŠธ 

ngrok http 5000

์ƒ์„ฑ๋œ URL(https://1234.ngrok.io/slack)์„ Slack API์˜ Event Subscriptions์— ๋“ฑ๋ก 

 

 

4) TeamCity ์„ค์ •

๐Ÿ“Œ Unity ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ์„ค์ •

1. TeamCity์—์„œ +New Bulid Configuration ํด๋ฆญ

 

2. Git ์ €์žฅ์†Œ ์—ฐ๊ฒฐ

* version Control Settings -> GitHub / GitLab ์ €์žฅ์†Œ ์—ฐ๊ฒฐ

* Branch : develop ๋˜๋Š” main

 

3. Build Steps์—์„œ Unity ๋นŒ๋“œ ์ถ”๊ฐ€

*  Runner Type : Command Line

 

 

์˜ต์…˜ ์„ค๋ช…
-batchmode Unity๋ฅผ UI ์—†์ด ์‹คํ–‰
-nographics ๊ทธ๋ž˜ํ”ฝ ํ™˜๊ฒฝ ์—†์ด ์‹คํ–‰
-quit ์‹คํ–‰ ํ›„ ์ž๋™ ์ข…๋ฃŒ
-projectPath Unity ํ”„๋กœ์ ํŠธ ํด๋” ๊ฒฝ๋กœ
-executeMethod C# ๋นŒ๋“œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
-logFile ๋นŒ๋“œ ๋กœ๊ทธ ์ €์žฅ ์œ„์น˜

 

 

 

5) Slack API ์„ธํŒ… ๋ฐ ์‹คํ–‰

๐Ÿ“Œ Slack API ์„ธํŒ…

1. ngrok http 5000์„ ์‹คํ–‰

 

 

2. ํ•ด๋‹น ์ฃผ์†Œ๋ฅผ Slack API ๊ด€๋ฆฌ ํŽ˜์ด์ง€์˜ Interactivity & Shortcuts ์˜ Request URL์— ๋“ฑ๋กํ•ด์ค€๋‹ค.

 

์ฃผ์˜์‚ฌํ•ญ. ๋งจ ๋’ค์— /slack ๋ถ™์—ฌ์ค˜์•ผ ํ•จ! 

 

3. Event Subscriptions์˜ Request URL์—๋„ ๋™์ผํ•˜๊ฒŒ URL ์ž…๋ ฅ

4. Event Subscriptions์— Subscribe to bot events์— Add Bot User Event ์ถ”๊ฐ€

 

 

๐Ÿ“Œ ์‹คํ–‰ํ•ด๋ณด๊ธฐ

1. python ํŒŒ์ผ ์‹คํ–‰ํ•˜๊ธฐ

python slack_bot.py

 

2. ๋ด‡์„ ์ดˆ๋Œ€ํ•œ ์Šฌ๋ž™์˜ ์ฑ„๋„์—์„œ !๋นŒ๋“œ๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ํ™•์ธํ•ด์ฃผ๊ธฐ

ํœด ์™„์„ฑ;

 

BELATED ARTICLES

more