Repository: iamhoonse-dev/turborepo-template
Pull request #128: 모든 테스트들을 하나의 워크플로우에서 실행하도록 변경하여 CI 테스트 실행 속도 개선
Author: login: iamhoonse-dev
Labels: name: enhancement
description: New feature or request
name: testing
description: 테스팅 관련 작업
name: github actions
description: GitHub Actions 관련 변경 작업
Created: 2025-05-17T18:51:16Z | Merged: 2025-05-18T04:10:49Z

This pull request introduces significant updates to the CI/CD workflows and adds a new Docker Compose configuration for testing. The changes aim to enhance caching strategies, streamline workflows, and enable containerized testing environments for various test suites, including Lighthouse, E2E, and Storybook.

### Workflow Enhancements

* **Improved caching strategies in `release.yml`:**
  - Updated cache keys for Turbo, `node_modules`, and `dist` directories to include `pnpm-lock.yaml` and `pnpm-workspace.yaml` hashes for more granular cache invalidation. [[1]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L33-R36) [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34R49-R52) [[3]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L60-R64)
  - Added `restore-keys` for `node_modules` and `dist` caches to improve cache restoration fallback. [[1]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34R49-R52) [[2]](diffhunk://#diff-87db21a973eed4fef5f32b267aa60fcee5cbdf03c67fafdc2a9b553bb0b15f34L60-R64)

* **Renamed and expanded `test.yml` workflow:**
  - Renamed the workflow from `unit-test` to `test` to reflect its broader scope.
  - Added environment variable `DOCKER_BUILDKIT` to enable Docker BuildKit for improved build performance.

### Containerized Testing

* **New jobs for containerized testing in `test.yml`:**
  - Added `build-docker` job to build Docker images using `docker-compose.gha.yaml`.
  - Introduced `lighthouse-test`, `e2e-web`, and `storybook-test` jobs, each running specific test suites in isolated Docker containers. These jobs depend on the `build-docker` job and utilize a shared external Docker network.

### New Configuration for Docker Compose

* **Added `docker-compose.gha.yaml`:**
  - Defined services for `web`, `playwright-web`, `lighthouse-runner`, `storybook`, and `storybook-test-runner` with build caching and dependency relationships.
  - Configured an external Docker network (`test_network`) to facilitate inter-service communication during testing.


Diff:
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
deleted file mode 100644
index fd72e93b..00000000
--- a/.github/workflows/e2e-test.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: e2e-test
-
-on:
-  push:
-    branches:
-      - '*'
-
-env:
-  DOCKER_BUILDKIT: 1
-
-jobs:
-  run-docker-compose-e2e:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Ensure Docker cache directory exists
-        run: mkdir -p /tmp/docker-cache
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Set up Docker Build
-        uses: docker/setup-buildx-action@v2
-
-      - name: inject environment variables
-        run: |
-          docker compose -f docker-compose.ci.e2e.yaml convert > docker-compose.e2e.converted.yaml
-        env:
-          REPOSITORY_OWNER: ${{ github.repository_owner }}
-
-      - name: Build E2E environment
-        id: build
-        run: |
-          docker compose -f docker-compose.e2e.converted.yaml build
-
-      - name: Prepare docker external network
-        run: |
-          docker network inspect "e2e_network" >/dev/null 2>&1 || docker network create "e2e_network"
-
-      - name: Run E2E tests
-        run: |
-          docker compose -f docker-compose.e2e.converted.yaml up --exit-code-from playwright-web
-
-      - name: Copy E2E report from container
-        run: |
-          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-playwright-web")
-          if [ -n "$CONTAINER_ID" ]; then
-            docker cp $CONTAINER_ID:/app/tools/playwright-web/test-results/junit.xml ./junit.xml
-          else
-            echo "No container found for playwright-web"
-            exit 1
-          fi
-
-      - name: Upload E2E report
-        uses: actions/upload-artifact@v4
-        with:
-          name: e2e-report
-          path: ./junit.xml
diff --git a/.github/workflows/lighthouse-test.yml b/.github/workflows/lighthouse-test.yml
deleted file mode 100644
index 1e162663..00000000
--- a/.github/workflows/lighthouse-test.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: lighthouse-test
-
-on:
-  push:
-    branches:
-      - '*'
-
-env:
-  DOCKER_BUILDKIT: 1
-
-jobs:
-  run-docker-compose-lighthouse:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Ensure Docker cache directory exists
-        run: mkdir -p /tmp/docker-cache
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Set up Docker Build
-        uses: docker/setup-buildx-action@v2
-
-      - name: inject environment variables
-        run: |
-          docker compose -f docker-compose.ci.lighthouse.yaml convert > docker-compose.lighthouse.converted.yaml
-        env:
-          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
-          REPOSITORY_OWNER: ${{ github.repository_owner }}
-
-      - name: Build Lighthouse environment
-        id: build
-        run: |
-          docker compose -f docker-compose.lighthouse.converted.yaml build
-
-      - name: Prepare docker external network
-        run: |
-          docker network inspect "lighthouse_network" >/dev/null 2>&1 || docker network create "lighthouse_network"
-
-      - name: Run Lighthouse tests
-        run: |
-          docker compose -f docker-compose.lighthouse.converted.yaml up --exit-code-from lighthouse-runner
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 40f7af4b..639ad910 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -30,10 +30,10 @@ jobs:
           path: |
             .turbo
             node_modules/.cache/turbo
-          key: turbo-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
+          key: ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
           restore-keys: |
-            turbo-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
-            turbo-cache-${{ runner.os }}-
+            ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
+            ${{ runner.os }}-turbo-cache-
 
       - name: Restore node_modules cache
         uses: actions/cache@v4
@@ -46,8 +46,10 @@ jobs:
             configs/*/node_modules
             shared/*/node_modules
           key: ${{ runner.os }}-node-modules-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}
+          restore-keys: |
+            ${{ runner.os }}-node-modules-
 
-      - name: Cache dist directories
+      - name: Restore dist cache
         uses: actions/cache@v4
         with:
           path: |
@@ -57,8 +59,9 @@ jobs:
             packages/*/dist
             configs/*/dist
             shared/*/dist
-          key: ${{ runner.os }}-dist-${{ github.sha }}
+          key: ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
           restore-keys: |
+            ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
             ${{ runner.os }}-dist-
 
       - name: Install Dependencies
diff --git a/.github/workflows/storybook-test.yml b/.github/workflows/storybook-test.yml
deleted file mode 100644
index eb5ed18d..00000000
--- a/.github/workflows/storybook-test.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: storybook-test
-
-on:
-  push:
-    branches:
-      - '*'
-
-env:
-  DOCKER_BUILDKIT: 1
-
-jobs:
-  run-docker-compose-storybook:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Ensure Docker cache directory exists
-        run: mkdir -p /tmp/docker-cache
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Set up Docker Build
-        uses: docker/setup-buildx-action@v2
-
-      - name: inject environment variables
-        run: |
-          docker compose -f docker-compose.ci.storybook.yaml convert > docker-compose.storybook.converted.yaml
-        env:
-          REPOSITORY_OWNER: ${{ github.repository_owner }}
-
-      - name: Build Storybook test environment
-        id: build
-        run: |
-          docker compose -f docker-compose.storybook.converted.yaml build
-
-      - name: Prepare docker external network
-        run: |
-          docker network inspect "storybook_network" >/dev/null 2>&1 || docker network create "storybook_network"
-
-      - name: Run Storybook tests
-        run: |
-          docker compose -f docker-compose.storybook.converted.yaml up --exit-code-from storybook-test-runner
-
-      - name: Copy Storybook report from container
-        run: |
-          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-storybook-test-runner")
-          if [ -n "$CONTAINER_ID" ]; then
-            docker cp $CONTAINER_ID:/app/apps/frontend-workshop/test-results/a11y-audit ./
-          else
-            echo "No container found for storybook-test-runner"
-            exit 1
-          fi
-
-      - name: Upload Storybook A11y report
-        uses: actions/upload-artifact@v4
-        with:
-          name: storybook-a11y-report
-          path: ./a11y-audit
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..bf866c28
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,249 @@
+name: test
+
+on:
+  push:
+    branches:
+      - '*'
+
+env:
+  DOCKER_BUILDKIT: 1
+  COMPOSE_BAKE: true
+
+jobs:
+  unit-test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: pnpm/action-setup@v4
+      - name: Use Node.js 22.x
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: .nvmrc
+          cache: 'pnpm'
+      - name: Restore turbo cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            .turbo
+            node_modules/.cache/turbo
+          key: ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
+          restore-keys: |
+            ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
+            ${{ runner.os }}-turbo-cache-
+      - name: Restore node_modules cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            node_modules
+            apps/*/node_modules
+            tools/*/node_modules
+            packages/*/node_modules
+            configs/*/node_modules
+            shared/*/node_modules
+          key: ${{ runner.os }}-node-modules-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}
+          restore-keys: |
+            ${{ runner.os }}-node-modules-
+      - name: Restore dist cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            apps/*/.next
+            !apps/*/.next/cache
+            apps/*/storybook-static
+            packages/*/dist
+            configs/*/dist
+            shared/*/dist
+          key: ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
+          restore-keys: |
+            ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
+            ${{ runner.os }}-dist-
+      - name: Restore unit test cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            coverage.json
+            apps/*/coverage.json
+            packages/*/coverage.json
+            configs/*/coverage.json
+            shared/*/coverage.json
+            tools/*/coverage.json
+          key: ${{ runner.os }}-unittest-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
+          restore-keys: |
+            ${{ runner.os }}-unittest-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
+            ${{ runner.os }}-unittest-
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile
+      - name: Build project
+        run: |
+          TURBO_LOG_LEVEL=debug pnpm run build --summarize > turbo-build-stdout.log
+          summary_path=$(cat turbo-build-stdout.log | tail -n 2 | head -n 1 | sed 's/^Summary:    //' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+          if [ -z "$summary_path" ]; then
+            echo "No summary path found in turbo-build-stdout.log"
+            exit 1
+          fi
+          cp $summary_path ./turbo-build-summarize.log
+      - name: Run unit tests
+        run: |
+          TURBO_LOG_LEVEL=debug pnpm run test --summarize > turbo-test-stdout.log
+          summary_path=$(cat turbo-test-stdout.log | tail -n 2 | head -n 1 | sed 's/^Summary:    //' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+          if [ -z "$summary_path" ]; then
+            echo "No summary path found in turbo-test-stdout.log"
+            exit 1
+          fi
+          cp $summary_path ./turbo-test-summarize.log
+      - name: Generate coverage reports
+        run: pnpm run merge-json-reports
+      - name: Upload coverage reports
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-reports
+          path: configs/vitest-config/coverage/merged
+      - name: Upload turbo test summarize log
+        uses: actions/upload-artifact@v4
+        with:
+          name: turbo-test-summarize
+          path: |
+            turbo-test-stdout.log
+            turbo-test-summarize.log
+      - name: Upload turbo build summarize log
+        uses: actions/upload-artifact@v4
+        with:
+          name: turbo-build-summarize
+          path: |
+            turbo-build-stdout.log
+            turbo-build-summarize.log
+
+  set-docker-compose:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Restore converted docker-compose file cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            docker-compose.gha.converted.yaml
+          key: ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          restore-keys: |
+            ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-
+            ${{ runner.os }}-docker-compose-
+      - name: Set up Docker Build
+        uses: docker/setup-buildx-action@v2
+      - name: inject environment variables
+        run: |
+          docker compose -f docker-compose.gha.yaml convert > docker-compose.gha.converted.yaml
+        env:
+          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          REPOSITORY_OWNER: ${{ github.repository_owner }}
+
+  lighthouse-test:
+    needs: set-docker-compose
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Restore converted docker-compose file cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            docker-compose.gha.converted.yaml
+          key: ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          restore-keys: |
+            ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-
+            ${{ runner.os }}-docker-compose-
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run Lighthouse tests
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up web lighthouse-runner --exit-code-from lighthouse-runner
+
+  e2e-web:
+    needs: set-docker-compose
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Restore converted docker-compose file cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            docker-compose.gha.converted.yaml
+          key: ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          restore-keys: |
+            ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-
+            ${{ runner.os }}-docker-compose-
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run E2E tests for web
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up web playwright-web --exit-code-from playwright-web
+      - name: Copy E2E report from container
+        run: |
+          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-playwright-web")
+          if [ -n "$CONTAINER_ID" ]; then
+            docker cp $CONTAINER_ID:/app/tools/playwright-web/test-results/junit.xml ./junit.xml
+          else
+            echo "No container found for playwright-web"
+            exit 1
+          fi
+      - name: Upload E2E report
+        uses: actions/upload-artifact@v4
+        with:
+          name: e2e-report
+          path: ./junit.xml
+
+  storybook-test:
+    needs: set-docker-compose
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Restore converted docker-compose file cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            docker-compose.gha.converted.yaml
+          key: ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          restore-keys: |
+            ${{ runner.os }}-docker-compose-${{ github.repository_owner }}-
+            ${{ runner.os }}-docker-compose-
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run Storybook tests
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up storybook storybook-test-runner --exit-code-from storybook-test-runner
+      - name: Copy Storybook report from container
+        run: |
+          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-storybook-test-runner")
+          if [ -n "$CONTAINER_ID" ]; then
+            docker cp $CONTAINER_ID:/app/apps/frontend-workshop/test-results/a11y-audit ./
+          else
+            echo "No container found for storybook-test-runner"
+            exit 1
+          fi
+      - name: Upload Storybook A11y report
+        uses: actions/upload-artifact@v4
+        with:
+          name: storybook-a11y-report
+          path: ./a11y-audit
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
deleted file mode 100644
index 08ca96f8..00000000
--- a/.github/workflows/unit-test.yml
+++ /dev/null
@@ -1,110 +0,0 @@
-name: unit-test
-
-on:
-  push:
-    branches:
-      - '*'
-
-jobs:
-  test:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-      - uses: pnpm/action-setup@v4
-      - name: Use Node.js 22.x
-        uses: actions/setup-node@v4
-        with:
-          node-version-file: .nvmrc
-          cache: 'pnpm'
-      - name: Restore turbo cache
-        uses: actions/cache@v4
-        with:
-          path: |
-            .turbo
-            node_modules/.cache/turbo
-          key: ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
-          restore-keys: |
-            ${{ runner.os }}-turbo-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
-            ${{ runner.os }}-turbo-cache-
-      - name: Restore node_modules cache
-        uses: actions/cache@v4
-        with:
-          path: |
-            node_modules
-            apps/*/node_modules
-            tools/*/node_modules
-            packages/*/node_modules
-            configs/*/node_modules
-            shared/*/node_modules
-          key: ${{ runner.os }}-node-modules-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}
-          restore-keys: |
-            ${{ runner.os }}-node-modules-
-      - name: Restore dist cache
-        uses: actions/cache@v4
-        with:
-          path: |
-            apps/*/.next
-            !apps/*/.next/cache
-            apps/*/storybook-static
-            packages/*/dist
-            configs/*/dist
-            shared/*/dist
-          key: ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
-          restore-keys: |
-            ${{ runner.os }}-dist-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
-            ${{ runner.os }}-dist-
-      - name: Restore unit test cache
-        uses: actions/cache@v4
-        with:
-          path: |
-            coverage.json
-            apps/*/coverage.json
-            packages/*/coverage.json
-            configs/*/coverage.json
-            shared/*/coverage.json
-            tools/*/coverage.json
-          key: ${{ runner.os }}-unittest-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-${{ github.sha }}
-          restore-keys: |
-            ${{ runner.os }}-unittest-${{ hashFiles('**/pnpm-lock.yaml', '**/pnpm-workspace.yaml') }}-
-            ${{ runner.os }}-unittest-
-      - name: Install dependencies
-        run: pnpm install --frozen-lockfile
-      - name: Build project
-        run: |
-          TURBO_LOG_LEVEL=debug pnpm run build --summarize > turbo-build-stdout.log
-          summary_path=$(cat turbo-build-stdout.log | tail -n 2 | head -n 1 | sed 's/^Summary:    //' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
-          if [ -z "$summary_path" ]; then
-            echo "No summary path found in turbo-build-stdout.log"
-            exit 1
-          fi
-          cp $summary_path ./turbo-build-summarize.log
-      - name: Run unit tests
-        run: |
-          TURBO_LOG_LEVEL=debug pnpm run test --summarize > turbo-test-stdout.log
-          summary_path=$(cat turbo-test-stdout.log | tail -n 2 | head -n 1 | sed 's/^Summary:    //' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
-          if [ -z "$summary_path" ]; then
-            echo "No summary path found in turbo-test-stdout.log"
-            exit 1
-          fi
-          cp $summary_path ./turbo-test-summarize.log
-      - name: Generate coverage reports
-        run: pnpm run merge-json-reports
-      - name: Upload coverage reports
-        uses: actions/upload-artifact@v4
-        with:
-          name: coverage-reports
-          path: configs/vitest-config/coverage/merged
-      - name: Upload turbo test summarize log
-        uses: actions/upload-artifact@v4
-        with:
-          name: turbo-test-summarize
-          path: |
-            turbo-test-stdout.log
-            turbo-test-summarize.log
-      - name: Upload turbo build summarize log
-        uses: actions/upload-artifact@v4
-        with:
-          name: turbo-build-summarize
-          path: |
-            turbo-build-stdout.log
-            turbo-build-summarize.log
diff --git a/docker-compose.gha.yaml b/docker-compose.gha.yaml
new file mode 100644
index 00000000..25db3f3c
--- /dev/null
+++ b/docker-compose.gha.yaml
@@ -0,0 +1,88 @@
+version: "3"
+
+# Define a network, which allows containers to communicate
+# with each other, by using their container name as a hostname
+networks:
+  test_network:
+    external: true
+
+# Define the services that will be run in the containers
+services:
+  # The web service is built from the Dockerfile located in the ./apps/web directory
+  web:
+    build:
+      context: .
+      dockerfile: ./apps/web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache,mode=max
+    ports:
+      - 3000:3000
+    networks:
+      - test_network
+
+  # The Playwright service is built from the Dockerfile located in the ./tools/playwright directory
+  playwright-web:
+    build:
+      context: .
+      dockerfile: ./tools/playwright-web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+
+  # The Lighthouse CI service is built from the Dockerfile located in the ./tools/lighthouse-ci directory
+  lighthouse-runner:
+    build:
+      context: .
+      dockerfile: ./tools/lighthouse-ci/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+      WEB_BASE_URL: http://web:3000
+      LHCI_GITHUB_APP_TOKEN: ${LHCI_GITHUB_APP_TOKEN}
+
+  # The frontend-workshop service is built from the Dockerfile located in the ./apps/frontend-workshop directory
+  storybook:
+    build:
+      context: .
+      dockerfile: ./apps/frontend-workshop/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/storybook:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/storybook:cache,mode=max
+    ports:
+      - 6006:80
+    networks:
+      - test_network
+
+  # The frontend-workshop-test-runner service is built from the Dockerfile located in the ./apps/frontend-workshop directory
+  storybook-test-runner:
+    build:
+      context: .
+      dockerfile: ./apps/frontend-workshop/Dockerfile.test-runner
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/storybook-test-runner:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/storybook-test-runner:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - storybook
+    environment:
+      CI: true
+      TARGET_URL: http://storybook:80


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR centralizes test execution by updating CI workflows and adding a Docker Compose configuration for containerized testing.  

- Enhanced `release.yml` caching with more granular keys and defined `restore-keys`  
- Renamed and expanded `test.yml` to `test`, set `DOCKER_BUILDKIT`, and introduced Docker-based jobs (`build-docker`, `lighthouse-test`, `e2e-web`, `storybook-test`)  
- Added `docker-compose.gha.yaml` to define services and an external network for isolated Lighthouse, E2E, and Storybook tests

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

| File                          | Description                                                                         |
|-------------------------------|-------------------------------------------------------------------------------------|
| docker-compose.gha.yaml       | New Docker Compose config defining test services, build cache, and external network |
| .github/workflows/test.yml    | Renamed workflow, enabled BuildKit, and added containerized test jobs               |
| .github/workflows/release.yml | Updated cache key patterns and restore-keys for Turbo, `node_modules`, and `dist`  |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**.github/workflows/test.yml:12**
* There appears to be an extra hyphen or misindentation before the `unit-test` job key which could break YAML parsing; ensure the job ID is correctly aligned under `jobs:`.
```
unit-test:
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T18:53:15Z
user: login: iamhoonse-dev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-17T19:45:17Z

Review comments:
diff_hunk: @@ -108,3 +111,88 @@ jobs:
           path: |
             turbo-build-stdout.log
             turbo-build-summarize.log
+
+  build-docker:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Set up Docker Build
+        uses: docker/setup-buildx-action@v2
+      - name: inject environment variables
+        run: |
+          docker compose -f docker-compose.gha.yaml convert > docker-compose.gha.converted.yaml
+        env:
+          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          REPOSITORY_OWNER: ${{ github.repository_owner }}
+      - name: Build Lighthouse environment
+        id: build
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml build
+
+  lighthouse-test:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run Lighthouse tests
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up --exit-code-from lighthouse-runner
+
+  e2e-web:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run E2E tests for web
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up --exit-code-from playwright-web
+      - name: Copy E2E report from container
+        run: |
+          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-playwright-web")
path: .github/workflows/test.yml
user: login: Copilot
body: Use `docker compose -f docker-compose.gha.converted.yaml ps -q playwright-web` instead of filtering by container name to reliably get the service container ID.
```suggestion
          CONTAINER_ID=$(docker compose -f docker-compose.gha.converted.yaml ps -q playwright-web)
```
created_at: 2025-05-17T18:53:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +111,88 @@ jobs:
           path: |
             turbo-build-stdout.log
             turbo-build-summarize.log
+
+  build-docker:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Set up Docker Build
+        uses: docker/setup-buildx-action@v2
+      - name: inject environment variables
+        run: |
+          docker compose -f docker-compose.gha.yaml convert > docker-compose.gha.converted.yaml
+        env:
+          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          REPOSITORY_OWNER: ${{ github.repository_owner }}
+      - name: Build Lighthouse environment
+        id: build
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml build
+
+  lighthouse-test:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run Lighthouse tests
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up --exit-code-from lighthouse-runner
+
+  e2e-web:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run E2E tests for web
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up --exit-code-from playwright-web
+      - name: Copy E2E report from container
+        run: |
+          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-playwright-web")
+          if [ -n "$CONTAINER_ID" ]; then
+            docker cp $CONTAINER_ID:/app/tools/playwright-web/test-results/junit.xml ./junit.xml
+          else
+            echo "No container found for playwright-web"
+            exit 1
+          fi
+      - name: Upload E2E report
+        uses: actions/upload-artifact@v4
+        with:
+          name: e2e-report
+          path: ./junit.xml
+
+  storybook-test:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
+        run: |
+          docker network inspect "test_network" >/dev/null 2>&1 || docker network create "test_network"
+      - name: Run Storybook tests
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml up --exit-code-from storybook-test-runner
+      - name: Copy Storybook report from container
+        run: |
+          CONTAINER_ID=$(docker ps -aqf "name=turborepo-template-storybook-test-runner")
path: .github/workflows/test.yml
user: login: Copilot
body: Similarly, use `docker compose -f docker-compose.gha.converted.yaml ps -q storybook-test-runner` to fetch the container ID instead of relying on the container name pattern.
```suggestion
          CONTAINER_ID=$(docker compose -f docker-compose.gha.converted.yaml ps -q storybook-test-runner)
```
created_at: 2025-05-17T18:53:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+version: "3"
+
+# Define a network, which allows containers to communicate
+# with each other, by using their container name as a hostname
+networks:
+  test_network:
+    external: true
+
+# Define the services that will be run in the containers
+services:
+  # The web service is built from the Dockerfile located in the ./apps/web directory
+  web:
+    build:
+      context: .
+      dockerfile: ./apps/web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache,mode=max
+    ports:
+      - 3000:3000
+    networks:
+      - test_network
+
+  # The Playwright service is built from the Dockerfile located in the ./tools/playwright directory
+  playwright-web:
+    build:
+      context: .
+      dockerfile: ./tools/playwright-web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+
+  # The Lighthouse CI service is built from the Dockerfile located in the ./tools/lighthouse-ci directory
+  lighthouse-runner:
+    build:
+      context: .
+      dockerfile: ./tools/lighthouse-ci/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+      WEB_BASE_URL: http://web:3000
+      LHCI_GITHUB_APP_TOKEN: ${LHCI_GITHUB_APP_TOKEN}
+
+  # The frontend-workshop service is built from the Dockerfile located in the ./apps/frontend-workshop directory
+  storybook:
+    build:
+      context: .
+      dockerfile: apps/frontend-workshop/Dockerfile
path: docker-compose.gha.yaml
user: login: Copilot
body: [nitpick] For consistency with other services, consider prefixing the path with `./` (e.g., `./apps/frontend-workshop/Dockerfile`) so all Dockerfile paths follow the same pattern.
```suggestion
      dockerfile: ./apps/frontend-workshop/Dockerfile
```
created_at: 2025-05-17T18:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,3 +111,88 @@ jobs:
           path: |
             turbo-build-stdout.log
             turbo-build-summarize.log
+
+  build-docker:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Login to GitHub Container Registry
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Set up Docker Build
+        uses: docker/setup-buildx-action@v2
+      - name: inject environment variables
+        run: |
+          docker compose -f docker-compose.gha.yaml convert > docker-compose.gha.converted.yaml
+        env:
+          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+          REPOSITORY_OWNER: ${{ github.repository_owner }}
+      - name: Build Lighthouse environment
+        id: build
+        run: |
+          docker compose -f docker-compose.gha.converted.yaml build
+
+  lighthouse-test:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    steps:
+      - name: Prepare docker external network
path: .github/workflows/test.yml
user: login: Copilot
body: [nitpick] This network setup step is duplicated across multiple jobs; consider extracting it into a reusable step or composite action to reduce repetition and simplify maintenance.
created_at: 2025-05-17T18:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+version: "3"
+
+# Define a network, which allows containers to communicate
+# with each other, by using their container name as a hostname
+networks:
+  test_network:
+    external: true
+
+# Define the services that will be run in the containers
+services:
+  # The web service is built from the Dockerfile located in the ./apps/web directory
+  web:
+    build:
+      context: .
+      dockerfile: ./apps/web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/web:cache,mode=max
+    ports:
+      - 3000:3000
+    networks:
+      - test_network
+
+  # The Playwright service is built from the Dockerfile located in the ./tools/playwright directory
+  playwright-web:
+    build:
+      context: .
+      dockerfile: ./tools/playwright-web/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/playwright-web:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+
+  # The Lighthouse CI service is built from the Dockerfile located in the ./tools/lighthouse-ci directory
+  lighthouse-runner:
+    build:
+      context: .
+      dockerfile: ./tools/lighthouse-ci/Dockerfile
+      cache_from:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache
+      cache_to:
+        - type=registry,ref=ghcr.io/${REPOSITORY_OWNER}/lighthouse-runner:cache,mode=max
+    networks:
+      - test_network
+    depends_on:
+      - web
+    environment:
+      CI: true
+      WEB_BASE_URL: http://web:3000
+      LHCI_GITHUB_APP_TOKEN: ${LHCI_GITHUB_APP_TOKEN}
+
+  # The frontend-workshop service is built from the Dockerfile located in the ./apps/frontend-workshop directory
+  storybook:
+    build:
+      context: .
+      dockerfile: apps/frontend-workshop/Dockerfile
path: docker-compose.gha.yaml
user: login: iamhoonse-dev
body: ### ✅ 반영 완료 !

[e6c6cd6](https://github.com/iamhoonse-dev/turborepo-template/pull/128/commits/e6c6cd6c850a53ec6e88acfa4b744574f099e0dd)에서 작업하였습니다 !
created_at: 2025-05-17T19:45:17Z
author_association: OWNER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-17T18:51:20Z
body: ###  ⚠️  No Changeset found

Latest commit: 76a46fccf83ee51b5645f5ca802d84af7c419954

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/iamhoonse-dev/turborepo-template/new/add-test-workflow?filename=.changeset/light-kings-clean.md&value=---%0A%0A---%0A%0A%EB%AA%A8%EB%93%A0%20%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%93%A4%EC%9D%84%20%ED%95%98%EB%82%98%EC%9D%98%20%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%97%90%EC%84%9C%20%EC%8B%A4%ED%96%89%ED%95%98%EB%8F%84%EB%A1%9D%20%EB%B3%80%EA%B2%BD%0A)


author_association: NONE
user: login: iamhoonse-dev
created_at: 2025-05-18T02:58:46Z
body: ## ⚖️ 동작 속도 비교 - 빌드된 Docker 이미지를 job 간에 전달하는 방식에 따라서

|👎 pull -> push|👍 두 번 빌드|
|-|-|
|- [사례 1 (7분 42초)](https://github.com/iamhoonse-dev/turborepo-template/actions/runs/15088772774)<br>- [사례 2 (12분 27초)](https://github.com/iamhoonse-dev/turborepo-template/actions/runs/15088772774)|[사례 1 (4분 12초)](https://github.com/iamhoonse-dev/turborepo-template/actions/runs/15088477548)|
|<img width="794" alt="image" src="https://github.com/user-attachments/assets/5f158d87-7757-4992-a0bc-339da2a4364e" /><img width="792" alt="image" src="https://github.com/user-attachments/assets/fe7d12aa-270f-4624-83b5-799c300befd1" />|<img width="791" alt="image" src="https://github.com/user-attachments/assets/8fbde564-5c42-402f-b5e0-8e104cf9868b" />|
author_association: OWNER
user: login: iamhoonse-dev
created_at: 2025-05-18T04:04:15Z
body: ### ✅ 최종 실행 결과

#### 1) [사례 1 - 3분 30초](https://github.com/iamhoonse-dev/turborepo-template/actions/runs/15092112606?pr=128)

<img width="794" alt="image" src="https://github.com/user-attachments/assets/20b7cde7-c6e9-4192-9184-002132c4b973" />

#### 2) [사례 2 - 3분 18초](https://github.com/iamhoonse-dev/turborepo-template/actions/runs/15092112606?pr=128)

<img width="792" alt="image" src="https://github.com/user-attachments/assets/c41e2bf9-c705-4a98-baa6-542f5579a5ed" />

author_association: OWNER