style: 대분류, 소분류, 세분류 pills의 스타일 개선 및 스크롤 기능 추가 #153
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Blog | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths-ignore: | |
| - 'content/posts/**' | |
| jobs: | |
| deploy: | |
| runs-on: self-hosted | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 10 | |
| - name: Determine current color | |
| id: color | |
| run: | | |
| CURRENT=$(docker ps --filter "name=blog-blue" --format "{{.Names}}" | grep -c blog-blue || true) | |
| if [ "$CURRENT" -gt "0" ]; then | |
| echo "current=blue" >> $GITHUB_OUTPUT | |
| echo "next=green" >> $GITHUB_OUTPUT | |
| echo "current_port=3000" >> $GITHUB_OUTPUT | |
| echo "next_port=3001" >> $GITHUB_OUTPUT | |
| else | |
| echo "current=green" >> $GITHUB_OUTPUT | |
| echo "next=blue" >> $GITHUB_OUTPUT | |
| echo "current_port=3001" >> $GITHUB_OUTPUT | |
| echo "next_port=3000" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Security scan | |
| run: | | |
| if [ -f "package.json" ]; then | |
| AUDIT_OUT=$(npm audit --omit=dev --json 2>&1) || true | |
| HIGH=$(echo "$AUDIT_OUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len([v for v in d.get('vulnerabilities',{}).values() if v.get('severity') in ('high','critical')]))" 2>/dev/null || echo "0") | |
| if [ "$HIGH" -gt "0" ]; then | |
| echo "::error::보안 스캔 실패 — HIGH/CRITICAL 취약점 ${HIGH}개 발견" | |
| echo "$AUDIT_OUT" > /tmp/blog_logs.txt | |
| exit 1 | |
| fi | |
| fi | |
| echo "Security scan passed." | |
| - name: Build and start next container | |
| env: | |
| NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} | |
| run: | | |
| docker build \ | |
| --build-arg NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL}" \ | |
| -t blog:${{ steps.color.outputs.next }} . | |
| docker stop blog-${{ steps.color.outputs.next }} || true | |
| docker rm blog-${{ steps.color.outputs.next }} || true | |
| docker run -d \ | |
| --name blog-${{ steps.color.outputs.next }} \ | |
| -p ${{ steps.color.outputs.next_port }}:3000 \ | |
| --restart always \ | |
| -e NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL}" \ | |
| -e CLAUDE_TOKEN=${{ secrets.CLAUDE_TOKEN }} \ | |
| -e ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }} \ | |
| -e JWT_SECRET=${{ secrets.JWT_SECRET }} \ | |
| -e GITHUB_TOKEN=${{ secrets.BLOG_GITHUB_TOKEN }} \ | |
| -e GITHUB_OWNER=dlekdns08 \ | |
| -e GITHUB_REPO=blog \ | |
| -e GITHUB_BRANCH=main \ | |
| -e ARXIV_DB_PATH=/data/arxiv_graph.db \ | |
| -v /app/actions-runner-arxiv/_work/arxiv-graph/arxiv-graph/data:/data:ro \ | |
| blog:${{ steps.color.outputs.next }} | |
| - name: Health check | |
| run: | | |
| sleep 5 | |
| for i in {1..15}; do | |
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${{ steps.color.outputs.next_port }} 2>/dev/null || echo "000") | |
| echo "Status: $STATUS ($i/15)" | |
| if [ "$STATUS" = "200" ]; then | |
| echo "Health check passed!" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "Health check failed!" | |
| docker stop blog-${{ steps.color.outputs.next }} || true | |
| docker rm blog-${{ steps.color.outputs.next }} || true | |
| exit 1 | |
| - name: Switch Nginx traffic | |
| run: | | |
| sudo sed -i "/server_name koala.ai.kr/,/}/s/localhost:[0-9]*/localhost:${{ steps.color.outputs.next_port }}/" /etc/nginx/sites-available/koala | |
| sudo nginx -t && sudo nginx -s reload | |
| - name: Remove old container | |
| run: | | |
| docker stop blog-${{ steps.color.outputs.current }} || true | |
| docker rm blog-${{ steps.color.outputs.current }} || true | |
| docker rmi blog:${{ steps.color.outputs.current }} || true | |
| - name: Notify subscribers on new post | |
| if: success() | |
| env: | |
| NOTIFY_API_KEY: ${{ secrets.NOTIFY_API_KEY }} | |
| run: | | |
| echo "=== DEBUG ===" | |
| echo "KEY_LENGTH=${#NOTIFY_API_KEY}" | |
| NEW_POST=$(git log --diff-filter=A --name-only --format= HEAD~5..HEAD -- 'content/posts/*.md' 'content/posts/**/*.md' 2>/dev/null | grep '\.md$' | head -1 || true) | |
| echo "NEW_POST=$NEW_POST" | |
| if [ -n "$NEW_POST" ] && [ -f "$NEW_POST" ]; then | |
| TITLE=$(grep -m1 '^title:' "$NEW_POST" 2>/dev/null | sed 's/^title:[[:space:]]*//' | tr -d '"' || basename "$NEW_POST" .md) | |
| REL=${NEW_POST#content/posts/} | |
| SLUG=${REL%.md} | |
| echo "SLUG=$SLUG" | |
| PAYLOAD=$(jq -n --arg title "$TITLE" --arg slug "$SLUG" --arg url "https://koala.ai.kr/posts/$SLUG" --arg key "$NOTIFY_API_KEY" \ | |
| '{"title":$title,"slug":$slug,"url":$url,"api_key":$key}') | |
| RESULT=$(curl -s -X POST https://api.koala.ai.kr/subscribe/notify \ | |
| -H 'Content-Type: application/json' \ | |
| -d "$PAYLOAD") | |
| echo "RESULT=$RESULT" | |
| else | |
| echo "새 글 없음 또는 파일 미존재" | |
| fi | |
| - name: Notify self-healing | |
| if: failure() | |
| run: | | |
| LOGS=$(cat /tmp/blog_logs.txt 2>/dev/null | tail -c 4000 || echo "Deploy failed") | |
| PAYLOAD=$(jq -n --arg repo "dlekdns08/blog" --arg logs "$LOGS" '{"repo":$repo,"logs":$logs}') | |
| curl -X POST http://5.104.87.242:8080/webhook/ci \ | |
| -H 'Content-Type: application/json' \ | |
| -H "x-ci-token: ${{ secrets.CI_WEBHOOK_TOKEN }}" \ | |
| -d "$PAYLOAD" |