Skip to content

fix: npm audit에서 devDependencies 검사 생략 및 jsPDF 의존성 보안 패치 #173

fix: npm audit에서 devDependencies 검사 생략 및 jsPDF 의존성 보안 패치

fix: npm audit에서 devDependencies 검사 생략 및 jsPDF 의존성 보안 패치 #173

Workflow file for this run

name: FrontEnd CI/CD
on:
pull_request:
paths:
- "src/**"
- "index.html"
- "package.json"
- "package-lock.json"
- "vite.config.js"
- "postcss.config.mjs"
- "eslint.config.js"
- "jsconfig.json"
- ".github/workflows/cicd.yml"
push:
branches: [ "main", "dev"]
workflow_dispatch:
inputs:
ci_run_id:
description: "Existing FrontEnd CI run-id to redeploy"
required: false
type: string
concurrency:
group: cd-frontend-main
cancel-in-progress: false
permissions:
id-token: write
actions: read
contents: read
security-events: write
jobs:
fe-ci:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.ci_run_id != '') }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [ "24" ]
steps:
# Step 1) Checkout
- name: Checkout
uses: actions/checkout@v4
# Step 1.1) Trivy Secret Scan
- name: Trivy Secret Scan
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: "fs"
scan-ref: "."
scanners: "secret"
format: "table"
exit-code: "1"
# Step 1.2) CodeQL Init (SAST 준비)
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
# Step 2) Setup Node
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
# Step 3) Cache (npm)
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node${{ matrix.node-version }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node${{ matrix.node-version }}-npm-
# Step 4) Install dependencies
- name: Install dependencies
run: npm ci
# Step 5) npm audit (취약점 검사)
- name: npm audit
run: npm audit --audit-level=high --omit=dev
# Step 6) Formatting check
- name: Formatting check
run: npm run format:check
# Step 6.1) Lint check
- name: Lint check
run: npm run lint
# Step 7) Vitest Tests
- name: Tests
env:
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}
run: npm run test:run -- --coverage
# Step 8) Build
- name: Build
env:
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}
run: npm run build
# Step 9) CodeQL Analyze (SAST 실행/업로드)
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:javascript-typescript"
# Step 10) Upload test artifacts
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: fe-test-artifacts-node${{ matrix.node-version }}
path: |
test-results/
coverage/
retention-days: 1
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: web-dist
path: dist/
if-no-files-found: error
retention-days: 1
deploy-current-run:
name: Deploy (current run)
needs: fe-ci
runs-on: ubuntu-latest
timeout-minutes: 20
if: >-
${{ needs.fe-ci.result == 'success' && github.event_name == 'push' && github.ref_name == 'main' }}
steps:
- name: Checkout (metadata only)
uses: actions/checkout@v4
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: dist
- name: Pack dist
shell: bash
run: |
set -euo pipefail
test -f dist/index.html
tar -C dist -czf dist.tgz .
- name: Upload dist.tgz to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
source: "dist.tgz"
target: "/tmp"
- name: Deploy via SSH (current run)
uses: appleboy/ssh-action@v1.0.3
env:
WEB_ROOT: ${{ vars.WEB_ROOT }}
NGINX_RELOAD: ${{ vars.NGINX_RELOAD }}
HEALTH_URL: ${{ vars.HEALTH_URL }}
HEALTH_TIMEOUT: ${{ vars.HEALTH_TIMEOUT }}
HEALTH_INTERVAL: ${{ vars.HEALTH_INTERVAL }}
BRANCH_NAME: ${{ github.ref_name }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
script_stop: true
command_timeout: 15m
envs: WEB_ROOT,NGINX_RELOAD,HEALTH_URL,HEALTH_TIMEOUT,HEALTH_INTERVAL,BRANCH_NAME
script: |
set -euo pipefail
WEB_ROOT="${WEB_ROOT:-/var/www/myweb}"
NGINX_RELOAD="${NGINX_RELOAD:-false}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1/}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
HEALTH_INTERVAL="${HEALTH_INTERVAL:-3}"
RELEASES_DIR="$WEB_ROOT/releases"
CURRENT_LINK="$WEB_ROOT/current"
TS="$(date +%Y%m%d-%H%M%S)"
NEW_DIR="$RELEASES_DIR/$TS"
echo "[1/7] prepare dirs"
sudo mkdir -p "$RELEASES_DIR"
sudo mkdir -p "$NEW_DIR"
echo "[2/7] extract dist into new release"
sudo tar -C "$NEW_DIR" -xzf /tmp/dist.tgz
sudo rm -f /tmp/dist.tgz
echo "[3/7] validate dist"
sudo test -f "$NEW_DIR/index.html"
echo "[4/7] atomic switch current -> new release"
sudo ln -sfn "$NEW_DIR" "$CURRENT_LINK"
echo "[5/7] (optional) nginx reload"
if [[ "$NGINX_RELOAD" == "true" ]]; then
sudo nginx -t
sudo systemctl reload nginx
fi
echo "[6/7] health check"
elapsed=0
until curl -fsS "$HEALTH_URL" > /dev/null; do
if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then
echo "Health check failed after ${HEALTH_TIMEOUT}s"
sudo systemctl status nginx --no-pager || true
exit 1
fi
sleep "$HEALTH_INTERVAL"
elapsed=$((elapsed + HEALTH_INTERVAL))
done
echo "[7/7] deploy done (release=$TS, branch=${BRANCH_NAME:-unknown})"
deploy-s3:
name: Deploy (S3 + CloudFront)
needs: fe-ci
runs-on: ubuntu-latest
timeout-minutes: 20
if: >-
${{ needs.fe-ci.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'}}
steps:
# 1) 저장소 체크아웃 (메타데이터만 필요)
- name: Checkout (metadata only)
uses: actions/checkout@v4
# 2) CI에서 생성된 dist 아티팩트 다운로드
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: dist
# 3) dist 최소 검증
- name: Validate dist
shell: bash
run: |
set -euo pipefail
test -f dist/index.html
# 4) AWS 인증 (OIDC 권장)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
# 5) S3 업로드 (동기화)
- name: Sync dist to S3
shell: bash
run: |
set -euo pipefail
aws s3 sync dist s3://${{ vars.S3_BUCKET_NAME }} \
--delete \
--cache-control "public,max-age=31536000,immutable" \
--exclude "index.html"
# index.html은 캐시 최소화 (SPA 갱신 대응)
aws s3 cp dist/index.html s3://${{ vars.S3_BUCKET_NAME }}/index.html \
--cache-control "no-cache,no-store,must-revalidate"
# 6) CloudFront 캐시 무효화
- name: Invalidate CloudFront cache
shell: bash
run: |
set -euo pipefail
aws cloudfront create-invalidation \
--distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
# 7) 배포 완료 로그
- name: Deploy done
run: echo "Deploy completed (S3 + CloudFront)"
deploy-reuse-artifact:
name: Deploy (reuse artifact)
runs-on: ubuntu-latest
timeout-minutes: 20
if: ${{ github.event_name == 'workflow_dispatch' && inputs.ci_run_id != '' }}
steps:
- name: Checkout (metadata only)
uses: actions/checkout@v4
- name: Download dist artifact (from run)
uses: actions/download-artifact@v4
with:
name: web-dist
path: dist
repository: ${{ github.repository }}
run-id: ${{ inputs.ci_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Pack dist
shell: bash
run: |
set -euo pipefail
test -f dist/index.html
tar -C dist -czf dist.tgz .
- name: Upload dist.tgz to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
source: "dist.tgz"
target: "/tmp"
- name: Deploy via SSH (reuse artifact)
uses: appleboy/ssh-action@v1.0.3
env:
WEB_ROOT: ${{ vars.WEB_ROOT }}
NGINX_RELOAD: ${{ vars.NGINX_RELOAD }}
HEALTH_URL: ${{ vars.HEALTH_URL }}
HEALTH_TIMEOUT: ${{ vars.HEALTH_TIMEOUT }}
HEALTH_INTERVAL: ${{ vars.HEALTH_INTERVAL }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
script_stop: true
command_timeout: 15m
envs: WEB_ROOT,NGINX_RELOAD,HEALTH_URL,HEALTH_TIMEOUT,HEALTH_INTERVAL
script: |
set -euo pipefail
WEB_ROOT="${WEB_ROOT:-/var/www/myweb}"
NGINX_RELOAD="${NGINX_RELOAD:-false}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1/}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
HEALTH_INTERVAL="${HEALTH_INTERVAL:-3}"
RELEASES_DIR="$WEB_ROOT/releases"
CURRENT_LINK="$WEB_ROOT/current"
TS="$(date +%Y%m%d-%H%M%S)"
NEW_DIR="$RELEASES_DIR/$TS"
echo "[1/7] prepare dirs"
sudo mkdir -p "$RELEASES_DIR"
sudo mkdir -p "$NEW_DIR"
echo "[2/7] extract dist into new release"
sudo tar -C "$NEW_DIR" -xzf /tmp/dist.tgz
sudo rm -f /tmp/dist.tgz
echo "[3/7] validate dist"
sudo test -f "$NEW_DIR/index.html"
echo "[4/7] atomic switch current -> new release"
sudo ln -sfn "$NEW_DIR" "$CURRENT_LINK"
echo "[5/7] (optional) nginx reload"
if [[ "$NGINX_RELOAD" == "true" ]]; then
sudo nginx -t
sudo systemctl reload nginx
fi
echo "[6/7] health check"
elapsed=0
until curl -fsS "$HEALTH_URL" > /dev/null; do
if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then
echo "Health check failed after ${HEALTH_TIMEOUT}s"
sudo systemctl status nginx --no-pager || true
exit 1
fi
sleep "$HEALTH_INTERVAL"
elapsed=$((elapsed + HEALTH_INTERVAL))
done
echo "[7/7] deploy done (release=$TS, run=${{ inputs.ci_run_id }})"