Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Type check
run: npm run typecheck

- name: Source file line limit
run: npm run check:source-lines

- name: Unit tests
run: npm test

Expand Down
165 changes: 73 additions & 92 deletions .github/workflows/external-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,15 @@ on:
permissions:
contents: read

concurrency:
group: external-smoke-${{ github.ref }}
cancel-in-progress: false

jobs:
external-smoke:
runs-on: ubuntu-latest
name: ${{ matrix.suite }} smoke (${{ matrix.service }})
concurrency:
group: external-smoke-${{ github.ref }}-${{ matrix.suite }}-${{ matrix.service }}
cancel-in-progress: false
strategy:
fail-fast: false
max-parallel: 4
matrix:
include:
- suite: cli
service: daiso
- suite: cli
service: gs25
- suite: cli
service: seveneleven
- suite: cli
service: emart24
- suite: cli
service: lottemart
- suite: cli
service: oliveyoung
- suite: cli
service: megabox
- suite: cli
service: lottecinema
- suite: cli
service: cgv
- suite: cli
service: opinet
- suite: mcp
service: daiso
- suite: mcp
service: gs25
- suite: mcp
service: seveneleven
- suite: mcp
service: emart24
- suite: mcp
service: opinet
timeout-minutes: 60
name: Sequential external smoke
steps:
- name: Checkout
uses: actions/checkout@v7
Expand All @@ -64,72 +31,75 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run external smoke
- name: Build once
run: npm run build

- name: Run external smoke sequentially
env:
SMOKE_SUITE: ${{ matrix.suite }}
SMOKE_SERVICE: ${{ matrix.service }}
CLI_SMOKE_SERVICES: daiso gs25 seveneleven emart24 lottemart oliveyoung megabox lottecinema cgv opinet
MCP_SMOKE_SERVICES: daiso gs25 seveneleven emart24 opinet
SMOKE_ATTEMPT_TIMEOUT_SECONDS: 90
run: |
write_summary() {
SMOKE_STATUS="$1" node <<'NODE'
const fs = require('node:fs');
const output = fs.existsSync('external-smoke-output.txt')
? fs.readFileSync('external-smoke-output.txt', 'utf8')
: '';
const excerpt = output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.slice(-8)
.join(' | ')
.slice(0, 1000);
const failure =
output
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) =>
/(?:CLI smoke 실패|CLI smoke 검증 실패|MCP smoke 실패|HTTP \d{3}|error code: \d+)/.test(
line,
),
) || 'unknown';
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const summary = [
`status=${process.env.SMOKE_STATUS || 'failed'}`,
`suite=${process.env.SMOKE_SUITE}`,
`service=${process.env.SMOKE_SERVICE}`,
`failure=${failure.slice(0, 300)}`,
`run=${runUrl}`,
excerpt,
].filter(Boolean).join(' ');
fs.writeFileSync('external-smoke-summary.txt', summary);
console.log(summary);
NODE
}
set +e
FAILURES=()

run_smoke_once() {
local SMOKE_SUITE="$1"
local SMOKE_SERVICE="$2"
local OUTPUT_FILE="$3"

run_smoke() {
if [ "${SMOKE_SUITE}" = "cli" ]; then
npm run cli:smoke -- --service "${SMOKE_SERVICE}" 2>&1 | tee external-smoke-output.txt
timeout --signal=TERM --kill-after=10s "${SMOKE_ATTEMPT_TIMEOUT_SECONDS}s" npx tsx scripts/ops/cli-smoke.ts --service "${SMOKE_SERVICE}" 2>&1 | tee "${OUTPUT_FILE}"
return "${PIPESTATUS[0]}"
fi

npm run mcp:smoke -- --service "${SMOKE_SERVICE}" 2>&1 | tee external-smoke-output.txt
timeout --signal=TERM --kill-after=10s "${SMOKE_ATTEMPT_TIMEOUT_SECONDS}s" npx tsx scripts/ops/mcp-smoke.ts --service "${SMOKE_SERVICE}" 2>&1 | tee "${OUTPUT_FILE}"
return "${PIPESTATUS[0]}"
}

if ! run_smoke; then
if [ "${SMOKE_SUITE}" = "cli" ]; then
echo "CLI smoke failed; retrying after 15 seconds"
else
echo "MCP smoke failed; retrying after 15 seconds"
run_smoke_with_retry() {
local SMOKE_SUITE="$1"
local SMOKE_SERVICE="$2"
local OUTPUT_FILE="external-smoke-${SMOKE_SUITE}-${SMOKE_SERVICE}.txt"

if run_smoke_once "${SMOKE_SUITE}" "${SMOKE_SERVICE}" "${OUTPUT_FILE}"; then
return 0
fi

echo "${SMOKE_SUITE}.${SMOKE_SERVICE} failed; retrying after 15 seconds"
sleep 15
if ! run_smoke; then
write_summary "failed"
exit 1
if run_smoke_once "${SMOKE_SUITE}" "${SMOKE_SERVICE}" "${OUTPUT_FILE}"; then
return 0
fi

local EXCERPT
EXCERPT="$(tail -n 8 "${OUTPUT_FILE}" | tr '\n' ' ' | cut -c1-1000)"
FAILURES+=("${SMOKE_SUITE}.${SMOKE_SERVICE}: ${EXCERPT:-unknown}")
return 1
}

for SMOKE_SERVICE in ${CLI_SMOKE_SERVICES}; do
run_smoke_with_retry "cli" "${SMOKE_SERVICE}" || true
done

for SMOKE_SERVICE in ${MCP_SMOKE_SERVICES}; do
run_smoke_with_retry "mcp" "${SMOKE_SERVICE}" || true
done

RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
if [ "${#FAILURES[@]}" -gt 0 ]; then
{
printf 'status=failed failures=%s run=%s ' "${#FAILURES[@]}" "${RUN_URL}"
printf '%s | ' "${FAILURES[@]}"
printf '\n'
} | tee external-smoke-summary.txt
exit 1
fi

echo "status=ok failures=0 run=${RUN_URL}" | tee external-smoke-summary.txt

- name: Notify smoke failure
if: failure()
if: failure() && github.event_name == 'schedule'
env:
MOSHI_WEBHOOK_TOKEN: ${{ secrets.MOSHI_WEBHOOK_TOKEN }}
run: |
Expand All @@ -138,7 +108,18 @@ jobs:
exit 0
fi

SUMMARY="$(cat external-smoke-summary.txt 2>/dev/null || echo 'external smoke command failed')"
curl -X POST https://api.getmoshi.app/api/webhook \
SUMMARY="$(cat external-smoke-summary.txt 2>/dev/null || echo 'external smoke command failed')" node <<'NODE'
const fs = require('node:fs');
fs.writeFileSync(
'moshi-payload.json',
JSON.stringify({
token: process.env.MOSHI_WEBHOOK_TOKEN,
title: 'External Smoke Failed',
message: `${process.env.GITHUB_REPOSITORY}: ${process.env.SUMMARY || ''}`.slice(0, 3500),
}),
);
NODE
curl --fail-with-body --silent --show-error \
-X POST https://api.getmoshi.app/api/webhook \
-H "Content-Type: application/json" \
-d "{\"token\":\"${MOSHI_WEBHOOK_TOKEN}\",\"title\":\"External Smoke Failed\",\"message\":\"${GITHUB_REPOSITORY}: ${SUMMARY}\"}"
--data-binary @moshi-payload.json
19 changes: 15 additions & 4 deletions .github/workflows/health-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
fi

- name: Notify health failure
if: failure()
if: failure() && github.event_name == 'schedule' && github.event.schedule == '10 15 * * *'
env:
MOSHI_WEBHOOK_TOKEN: ${{ secrets.MOSHI_WEBHOOK_TOKEN }}
run: |
Expand All @@ -87,7 +87,18 @@ jobs:
exit 0
fi

SUMMARY="$(cat health-check-summary.txt 2>/dev/null || echo 'health check command failed')"
curl -X POST https://api.getmoshi.app/api/webhook \
SUMMARY="$(cat health-check-summary.txt 2>/dev/null || echo 'health check command failed')" node <<'NODE'
const fs = require('node:fs');
fs.writeFileSync(
'moshi-payload.json',
JSON.stringify({
token: process.env.MOSHI_WEBHOOK_TOKEN,
title: 'Health Checks Failed',
message: `${process.env.GITHUB_REPOSITORY}: ${process.env.SUMMARY || ''}`.slice(0, 3500),
}),
);
NODE
curl --fail-with-body --silent --show-error \
-X POST https://api.getmoshi.app/api/webhook \
-H "Content-Type: application/json" \
-d "{\"token\":\"${MOSHI_WEBHOOK_TOKEN}\",\"title\":\"Health Checks Failed\",\"message\":\"${GITHUB_REPOSITORY}: ${SUMMARY}\"}"
--data-binary @moshi-payload.json
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
- **최대 줄 수**: 450줄
- **권장 줄 수**: 300-400줄
- **초과 시 조치**: 파일이 450줄을 초과하면 기능별로 분리하여 모듈화
- **예외**: 자동 생성 파일(예: OpenAPI 생성 산출물)은 예외로 둘 수 있음
- **자동 검사**: `src/**/*.ts`의 450줄 제한은 CI에서 자동으로 강제
- **예외**: 현재 `src` 아래 예외는 없으며 OpenAPI 생성 산출물은 저장소 루트에 위치

#### 파일 분리 예시

Expand Down
7 changes: 2 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@ ci: PR 검증 워크플로우 추가
- TypeScript `strict` 모드를 유지합니다.
- 비동기/외부 API 호출에는 에러 처리를 포함합니다.
- 주석은 한국어로 작성합니다.
- 파일 길이는 450줄 내외를 권장합니다.

예외:

- `src/pages/openapi.ts`는 OpenAPI 생성 로직 특성상 길어질 수 있습니다.
- 모든 `src/**/*.ts` 파일은 450줄 이하로 유지합니다.
- 450줄 제한은 CI에서 자동으로 강제되며 `npm run check:source-lines`로 로컬 확인합니다.
- 길이가 커지는 파일은 기능 단위 모듈 분리를 우선 검토하세요.

## 테스트 원칙
Expand Down
Loading