name: "CI/CD Pipeline"
on:
  push:
    branches-ignore:
      - l10n_develop
      - gh-pages
    paths-ignore:
      - '*.md'
      - 'LICENSE'
      - 'monitoring/grafana-dashboard.json'
      - 'screenshots/**'
    tags-ignore:
      - '*'
  pull_request:
    paths-ignore:
      - '*.md'
      - 'LICENSE'
      - 'data/static/i18n/*.json'
      - 'frontend/src/assets/i18n/*.json'
env:
  NODE_DEFAULT_VERSION: 24
  NODE_OPTIONS: "--max_old_space_size=4096"
  CYCLONEDX_NPM_VERSION: '^2.0.0||^3.0.0||^4.0.0'   # keep in sync with package.json
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js"
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ env.NODE_DEFAULT_VERSION }}
      - name: "Install application minimalistically"
        run: |
          npm install --ignore-scripts
          cd frontend
          npm install --ignore-scripts
      - name: "Lint source code"
        run: npm run lint
  coding-challenge-rsn:
    runs-on: windows-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js"
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ env.NODE_DEFAULT_VERSION }}
      - name: "Install application"
        run: npm install
      - name: "Check coding challenges for accidental code discrepancies"
        run: npm run rsn
  frontend-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [22, 24, 26]
    steps:
      - name: "Check out Git repository"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js ${{ matrix.node-version }}"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ matrix.node-version }}
      - name: "Install application"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        run: npm install
      - name: "Execute frontend unit tests"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: nick-invision/retry@7152eba30c6575329ac0576536151aca5a72780e #v3.0.0
        with:
          shell: bash
          timeout_minutes: 10
          max_attempts: 3
          command: npm run test:frontend:coverage
      - name: "Copy frontend unit test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        run: |
          cp frontend/coverage/frontend/lcov.info frontend-lcov.info
          sed -i s/SF:/SF:frontend\\//g frontend-lcov.info
        shell: bash
      - name: "Upload frontend unit test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
        with:
          name: frontend-test-lcov
          path: |
            frontend-lcov.info
  server-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [22, 24, 26]
    steps:
      - name: "Check out Git repository"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js ${{ matrix.node-version }}"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ matrix.node-version }}
      - name: "Install application"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        run: npm install
      - name: "Execute server unit tests"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: nick-invision/retry@7152eba30c6575329ac0576536151aca5a72780e #v3.0.0
        with:
          timeout_minutes: 10
          max_attempts: 3
          command: npm run test:server:coverage
      - name: "Copy server unit test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        run: |
          cp coverage/server-tests/lcov.info server-lcov.info
        shell: bash
      - name: "Upload server unit test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
        with:
          name: server-test-lcov
          path: |
            server-lcov.info
  api-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [22, 24, 26]
    steps:
      - name: "Check out Git repository"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js ${{ matrix.node-version }}"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ matrix.node-version }}
      - name: "Install application"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        run: npm install
      - name: "Execute integration tests"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        uses: nick-invision/retry@7152eba30c6575329ac0576536151aca5a72780e #v3.0.0
        env:
          NODE_ENV: test
          ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
        with:
          timeout_minutes: 5
          max_attempts: 3
          command: npm run test:api:coverage
      - name: "Copy API test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        run: cp coverage/api-tests/lcov.info api-lcov.info
      - name: "Upload API test coverage data"
        if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24'
        uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b #v4.5.0
        with:
          name: api-test-lcov
          path: |
            api-lcov.info
  coverage-report:
    needs: [frontend-test, server-test, api-test]
    runs-on: ubuntu-latest
    if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push'
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Download frontend test coverage data"
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
        with:
          name: frontend-test-lcov
      - name: "Download server test coverage data"
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
        with:
          name: server-test-lcov
      - name: "Download API test coverage data"
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8
        with:
          name: api-test-lcov
      - name: "Publish coverage to Coveralls"
        uses: coverallsapp/github-action@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          files: frontend-lcov.info server-lcov.info api-lcov.info
  custom-config-test:
    runs-on: ubuntu-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js"
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ env.NODE_DEFAULT_VERSION }}
      - name: "Install application"
        if: github.repository == 'juice-shop/juice-shop' || (github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24')
        run: npm install
      - name: "Execute server tests for each custom configuration"
        uses: nick-invision/retry@7152eba30c6575329ac0576536151aca5a72780e #v3.0.0
        with:
          timeout_minutes: 30
          max_attempts: 3
          command: >
            NODE_ENV=7ms npm run test:server &&
            NODE_ENV=addo npm run test:server &&
            NODE_ENV=bodgeit npm run test:server &&
            NODE_ENV=ctf npm run test:server &&
            NODE_ENV=fbctf npm run test:server &&
            NODE_ENV=juicebox npm run test:server &&
            NODE_ENV=mozilla npm run test:server &&
            NODE_ENV=oss npm run test:server &&
            NODE_ENV=quiet npm run test:server &&
            NODE_ENV=tutorial npm run test:server &&
            NODE_ENV=unsafe npm run test:server
  e2e-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
        browser: [chrome] # FIXME Switch back to [chrome, firefox] after debugging extreme flakiness of Firefox on CI/CD
      fail-fast: false
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js"
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ env.NODE_DEFAULT_VERSION }}
      - name: "Install CLI tools"
        run: npm install -g @angular/cli
      - name: "Install application"
        run: npm install
      - name: "Execute end-to-end tests on Ubuntu"
        if: ${{ matrix.os == 'ubuntu-latest' }}
        uses: cypress-io/github-action@57b70560982e6a11d23d4b8bec7f8a487cdbb71b #v6.7.8
        with:
          install: false
          browser: ${{ matrix.browser }}
          start: npm start
          wait-on: http://localhost:3000
          record: true
          group: ${{ matrix.browser }} @ ${{ matrix.os }}
        env:
          SOLUTIONS_WEBHOOK: ${{ secrets.E2E_SOLUTIONS_WEBHOOK }}
          ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: "Execute end-to-end tests on Mac"
        if: ${{ matrix.os == 'macos-latest' }}
        uses: cypress-io/github-action@57b70560982e6a11d23d4b8bec7f8a487cdbb71b #v6.7.8
        with:
          install: false
          browser: ${{ matrix.browser }}
          start: npm start
          wait-on: http://localhost:3000
          record: true
          group: ${{ matrix.browser }} @ ${{ matrix.os }}
        env:
          CYPRESS_CACHE_FOLDER: /Users/runner/Library/Caches/Cypress
          ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  smoke-test:
    runs-on: ubuntu-latest
    env:
      TYPESCRIPT_VERSION: '^6.0.3' # keep in sync with package.json
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Use Node.js"
        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
        with:
          node-version: ${{ env.NODE_DEFAULT_VERSION }}
      - name: "Install CLI tools"
        run: |
          npm install -g typescript@${{ env.TYPESCRIPT_VERSION }}
          npm install -g @cyclonedx/cyclonedx-npm@$CYCLONEDX_NPM_VERSION
          npm install -g grunt-cli
      - name: "Set packaging options for Grunt"
        run: |
          echo "PCKG_OS_NAME=linux" >> $GITHUB_ENV
          echo "PCKG_NODE_VERSION=24" >> $GITHUB_ENV
          echo "PCKG_CPU_ARCH=x64" >> $GITHUB_ENV
      - name: "Package application"
        run: |
          npm install --production
          npm run package:ci
      - name: "Unpack application archive"
        run: |
          cd dist
          tar -zxf juice-shop-*.tgz
      - name: "Execute smoke test"
        run: |
          cd dist/juice-shop_*
          npm start &
          cd ../..
          chmod +x test/smoke/smoke-test.sh
          test/smoke/smoke-test.sh http://localhost:3000
  docker-test:
    runs-on: ubuntu-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Execute smoke test on Docker"
        run: docker compose -f docker-compose.test.yml up --exit-code-from sut
  docker:
    if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')
    needs: [frontend-test, server-test, api-test, e2e-test, custom-config-test, docker-test]
    runs-on: ubuntu-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Set up QEMU"
        uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf #v3.2.0
      - name: "Set up Docker Buildx"
        uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 #v3.8.0
      - name: "Login to DockerHub"
        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: "Set tag & labels for ${{ github.ref }}"
        run: |
          if [ "$GITHUB_REF" == "refs/heads/master" ]; then
          echo "DOCKER_TAG=latest" >> $GITHUB_ENV
          else
          echo "DOCKER_TAG=snapshot" >> $GITHUB_ENV
          fi
          echo "VCS_REF=`git rev-parse --short HEAD`" >> $GITHUB_ENV
          echo "BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”`" >> $GITHUB_ENV
      - name: "Build and push for AMD64 and ARM64 processors"
        uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 #v6.1.0
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            bkimminich/juice-shop:${{ env.DOCKER_TAG }}
          build-args: |
            VCS_REF=${{ env.VCS_REF }}
            BUILD_DATE=${{ env.BUILD_DATE }}
            CYCLONEDX_NPM_VERSION=${{ env.CYCLONEDX_NPM_VERSION }}
  heroku:
    if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')
    needs: [frontend-test, server-test, api-test, e2e-test, custom-config-test]
    runs-on: ubuntu-latest
    steps:
      - name: "Check out Git repository"
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
      - name: "Install Heroku CLI"
        run: curl https://cli-assets.heroku.com/install.sh | sh
      - name: "Set Heroku app & branch for ${{ github.ref }}"
        run: |
          if [ "$GITHUB_REF" == "refs/heads/master" ]; then
          echo "HEROKU_APP=juice-shop" >> $GITHUB_ENV
          echo "HEROKU_BRANCH=master" >> $GITHUB_ENV
          else
          echo "HEROKU_APP=juice-shop-staging" >> $GITHUB_ENV
          echo "HEROKU_BRANCH=develop" >> $GITHUB_ENV
          fi
      - name: "Deploy ${{ github.ref }} to Heroku"
        uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 #v3.13.15
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: ${{ env.HEROKU_APP }}
          heroku_email: bjoern.kimminich@owasp.org
          branch: ${{ env.HEROKU_BRANCH }}
  notify-slack:
    if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (success() || failure())
    needs:
      - docker
      - heroku
      - lint
      - coding-challenge-rsn
      - smoke-test
      - coverage-report
    runs-on: ubuntu-latest
    steps:
      - name: "Slack workflow notification"
        uses: Gamesight/slack-workflow-status@68bf00d0dbdbcb206c278399aa1ef6c14f74347a #v1.3.0
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
