CollabOps

예제

수신 웹훅 예제 — curl 한 줄, 토큰, 스레드, blocks 형식, CI 워크플로 알림

복사해서 바로 쓸 수 있는 수신 웹훅 예제입니다. 모든 예제의 <토큰> · <시크릿> 은 발급받은 값으로 바꿉니다. 필드와 상한은 수신 웹훅, 서식과 토큰 전량은 표현 레퍼런스에 있습니다.

1. 가장 짧은 알림

curl -X POST "https://collabops.ai/api/v2/integration/webhook/<토큰>" \
  -H "Authorization: Bearer <시크릿>" \
  -H "Content-Type: application/json" \
  -d '{"text": "**배포 완료** `web` 0.4.2"}'

성공하면 {"delivery_id": 1234, "deduped": false} 가 돌아오고, 채널에 굵은 글씨와 코드 칩이 든 메시지가 올라갑니다.

2. 마크다운 서식과 토큰

여러 줄 본문은 파일로 만들어 --data-binary @파일 로 보내면 셸 따옴표 걱정이 없습니다. JSON 문자열 안의 줄바꿈은 \n 으로 씁니다.

{
  "title": "로그인 오류 핫픽스 배포",
  "text": "**핫픽스 배포 완료**\n{{issue:slug=acme|key=42|label=로그인 오류}} 수정분이 반영됐습니다.\n\n- 변경 요청: {{cr:slug=acme|key=318|label=세션 만료 수정}}\n- 배포 시각: {{date:iso=2026-09-23T15:30:00+09:00|hasTime=1}}\n\n| 서비스 | 버전 |\n|:--|--:|\n| api | 1.8.2 |\n| web | 0.4.2 |"
}
curl -X POST "https://collabops.ai/api/v2/integration/webhook/<토큰>" \
  -H "Authorization: Bearer <시크릿>" \
  -H "Content-Type: application/json" \
  --data-binary @message.json

title 은 채널 목록과 알림의 미리보기로만 쓰이고 본문에는 보이지 않습니다. 이슈·변경 요청은 칩으로, 날짜는 날짜 칩으로 바뀝니다. 채팅에서 제목(#)은 일반 글처럼 보이므로 첫 줄은 굵게로 강조했습니다.

예제 2 가 채널에 올라간 모습 — 굵은 첫 줄, 이슈·변경 요청·날짜 칩, 정렬된 표

그림 1 — 예제 2 가 채널에 올라간 모습

3. 스레드로 이어 보내기

첫 응답의 delivery_id 를 받아 두었다가 다음 요청의 parent_delivery_id 로 넣습니다. jq 가 없는 환경에서도 돌도록 sed 로 꺼냅니다.

URL="https://collabops.ai/api/v2/integration/webhook/<토큰>"
AUTH="Authorization: Bearer <시크릿>"

# 원글
RESP=$(curl -sS -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"text": "**야간 배치 시작**"}')
PARENT=$(printf '%s' "$RESP" | sed -n 's/.*"delivery_id":\([0-9]*\).*/\1/p')

# 답글 — 원글 스레드에 붙는다
curl -sS -X POST "$URL" -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"text\": \"1단계 완료 (12분)\", \"parent_delivery_id\": ${PARENT}}"

답글의 delivery_id 를 다시 parent_delivery_id 로 써도 새 스레드가 생기지 않고 같은 스레드에 붙습니다.

원글 아래 1개의 답글 표시와 오른쪽 스레드 패널의 답글

그림 2 — 답글은 원글의 스레드에 붙습니다

4. blocks 형식 — 색 · 표 · 토글 · 이슈 칩

마크다운으로 안 되는 표현이 필요하면 blocks 로 보냅니다. 아래는 점검 결과를 표로 보여 주고, 실패 로그는 토글 안에 접어 두는 예입니다.

{
  "blocks": [
    {"id": "title", "type": "paragraph",
     "children": [{"text": "야간 점검 결과", "marks": [{"type": "bold"}]}]},

    {"id": "summary", "type": "paragraph", "children": [
      {"text": "3건 중 "},
      {"text": "1건 실패", "marks": [{"type": "bold"}, {"type": "textColor", "attrs": {"color": "red"}}]},
      {"text": " — 추적 "},
      {"type": "embed", "embedType": "entityRef",
       "attrs": {"kind": "issue", "workspaceSlug": "acme", "entityKey": 57, "title": "인증서 자동 갱신"}}
    ]},

    {"id": "c00", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 0, "colIdx": 0, "colCount": 2},
     "children": [{"text": "점검"}]},
    {"id": "c01", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 0, "colIdx": 1},
     "children": [{"text": "결과"}]},
    {"id": "c10", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 1, "colIdx": 0},
     "children": [{"text": "DB 백업"}]},
    {"id": "c11", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 1, "colIdx": 1},
     "children": [{"text": "성공", "marks": [{"type": "textColor", "attrs": {"color": "green"}}]}]},
    {"id": "c20", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 2, "colIdx": 0},
     "children": [{"text": "인증서 만료"}]},
    {"id": "c21", "type": "tableCell", "attrs": {"tableId": "t1", "rowIdx": 2, "colIdx": 1},
     "children": [{"text": "7일 남음", "marks": [{"type": "textColor", "attrs": {"color": "red"}}]}]},

    {"id": "log", "type": "toggle", "children": [{"text": "실패 로그 보기"}]},
    {"id": "log-body", "parentId": "log", "type": "codeBlock", "attrs": {"language": "text"},
     "children": [{"text": "cert-check: api.example.com expires in 7 days\nexit 1"}]}
  ]
}
curl -X POST "https://collabops.ai/api/v2/integration/webhook/<토큰>" \
  -H "Authorization: Bearer <시크릿>" \
  -H "Content-Type: application/json" \
  --data-binary @nightly.json

예제 4 가 채널에 올라간 모습 — 빨간 1건 실패, 이슈 칩, 색이 칠해진 표, 펼친 토글

그림 3 — 예제 4 를 토글을 펼친 상태로 본 모습

포인트

표는 셀 하나가 블록 하나입니다. 이 예제는 헤더 포함 6셀이라 표만으로 6블록, 전체 10블록입니다.

colCount 는 표의 첫 셀(rowIdx: 0, colIdx: 0)에만 적습니다.

표 칸의 색은 칸의 attrs 가 아니라 칸 안 글 조각에 textColor 마크로 칠합니다. 블록 attrs 의 색은 채팅에서 칠해지지 않습니다.

토글 안에 넣을 블록은 parentId 로 토글의 id 를 가리키고, 배열에서 토글 바로 뒤에 둡니다. 토글은 접힌 채 올라가고 누르면 펼쳐집니다.

블록 하나라도 규격에 어긋나면 메시지 전체가 422 로 거절되고 error.title 에 몇 번째 블록인지 나옵니다.

5. CI 워크플로에서 성공 · 실패 알림

배포 job 뒤에 성공 알림과 실패 알림 job 을 따로 둡니다. 알림은 부가 기능이므로 알림 요청이 실패해도 파이프라인은 초록으로 끝나야 합니다. curl 의 실패를 || HTTP_CODE="000" 으로 받아 응답 코드만 로그에 남기는 이유입니다.

name: deploy

triggers:
  push:
    branches: [main]

jobs:
  deploy:
    steps:
      - name: deploy
        run: ./scripts/deploy.sh               # 실제 배포 명령

  # ─────────────────────────────────────────
  # 배포 성공 알림 — deploy 가 성공했을 때만
  # ─────────────────────────────────────────
  notify-success:
    needs: [deploy]
    if: "success()"
    steps:
      - name: notify
        image: curlimages/curl:8.17.0          # curl 만 있으면 되는 가벼운 이미지
        env:
          # URL 과 시크릿은 서로 다른 값이다 — 시크릿 저장소에 따로 넣는다
          COLLABOPS_WEBHOOK_URL: ${{ secrets.COLLABOPS_WEBHOOK_URL }}
          COLLABOPS_WEBHOOK_SECRET: ${{ secrets.COLLABOPS_WEBHOOK_SECRET }}
          REF_NAME: ${{ collabops.ref_name }}
          COMMIT_SHA: ${{ collabops.sha }}
          COMMIT_MESSAGE: ${{ collabops.commit_message }}
          ACTOR: ${{ collabops.actor }}
        run: |
          # JSON 문자열에 넣기 전에 역슬래시와 큰따옴표를 이스케이프한다
          esc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }

          TITLE=$(esc "$(printf '%s\n' "$COMMIT_MESSAGE" | head -1)")   # 커밋 제목 한 줄만
          SHORT_SHA=$(printf '%s' "$COMMIT_SHA" | cut -c1-8)

          BODY=$(printf '{"text": "✅ **배포 성공** `%s` `%s`\\n%s\\n실행자: %s"}' \
            "$REF_NAME" "$SHORT_SHA" "$TITLE" "$(esc "$ACTOR")")

          # 알림이 실패해도 파이프라인을 실패시키지 않는다 — 응답 코드만 로그에 남긴다
          HTTP_CODE=$(curl -sS -o /tmp/webhook-resp -w '%{http_code}' -X POST \
            -H "Authorization: Bearer ${COLLABOPS_WEBHOOK_SECRET}" \
            -H 'Content-Type: application/json' \
            -H "Idempotency-Key: deploy-${SHORT_SHA}-success" \
            --data "$BODY" \
            "${COLLABOPS_WEBHOOK_URL}") || HTTP_CODE="000"
          echo "webhook http=${HTTP_CODE} $(cat /tmp/webhook-resp 2>/dev/null)"

  # ─────────────────────────────────────────
  # 배포 실패 알림 — deploy 가 실패했을 때만
  # ─────────────────────────────────────────
  notify-failure:
    needs: [deploy]
    if: "failure()"
    steps:
      - name: notify
        image: curlimages/curl:8.17.0
        env:
          COLLABOPS_WEBHOOK_URL: ${{ secrets.COLLABOPS_WEBHOOK_URL }}
          COLLABOPS_WEBHOOK_SECRET: ${{ secrets.COLLABOPS_WEBHOOK_SECRET }}
          REF_NAME: ${{ collabops.ref_name }}
          COMMIT_SHA: ${{ collabops.sha }}
        run: |
          SHORT_SHA=$(printf '%s' "$COMMIT_SHA" | cut -c1-8)
          BODY=$(printf '{"text": "❌ **배포 실패** `%s` `%s`\\n실행 로그를 확인해 주세요."}' \
            "$REF_NAME" "$SHORT_SHA")
          HTTP_CODE=$(curl -sS -o /tmp/webhook-resp -w '%{http_code}' -X POST \
            -H "Authorization: Bearer ${COLLABOPS_WEBHOOK_SECRET}" \
            -H 'Content-Type: application/json' \
            -H "Idempotency-Key: deploy-${SHORT_SHA}-failure" \
            --data "$BODY" \
            "${COLLABOPS_WEBHOOK_URL}") || HTTP_CODE="000"
          echo "webhook http=${HTTP_CODE} $(cat /tmp/webhook-resp 2>/dev/null)"

실행 흐름

deploy
  ├── 성공 → notify-success  (✅ 배포 성공 알림)
  └── 실패 → notify-failure  (❌ 배포 실패 알림)

알림 요청이 실패해도 → 로그에 http=000 또는 4xx 만 남고 job 은 성공

포인트

URL 과 시크릿을 시크릿 저장소에 따로 넣고 ${{ secrets.… }} 로 읽습니다. 워크플로 파일에 값을 적지 않습니다.

커밋 메시지처럼 따옴표나 역슬래시가 들어갈 수 있는 값은 esc 로 이스케이프한 뒤 JSON 에 넣습니다. 이스케이프하지 않으면 본문이 JSON 이 아니게 되어 422 가 납니다.

Idempotency-Key 에 커밋 SHA 와 결과를 넣어 두면 job 을 다시 실행해도 같은 알림이 두 번 뜨지 않습니다.

워크플로 문법은 CI/CD 워크플로 문법을, 조건식은 표현식을 참고하세요.

6. 응답 코드 확인하기

스크립트에서 결과를 판단하려면 -w '%{http_code}' 로 상태 코드를 따로 받습니다. 202 가 아니면 본문의 error.title 에 사유가 있습니다.

CODE=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$URL" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"text": "점검 완료", "status": "ok"}')
echo "$CODE"; cat resp.json
# 422
# {"error":{"code":"VALIDATION_ERROR","title":"요청 형식이 올바르지 않습니다: extra_forbidden at status", …}}

받지 않는 필드(status)를 넣어서 422 가 났습니다. text 형식이 받는 필드는 text · title · parent_delivery_id 뿐입니다.

목차