name: Build & Test

on:
  push:
    branches:
      - canary
      - beta
      - stable
      - v[0-9]+.[0-9]+.x-staging
      - v[0-9]+.[0-9]+.x
    paths-ignore:
      - README.md
  pull_request:

env:
  DEBUG: napi:*
  BUILD_TYPE: canary
  APP_NAME: affine
  AFFINE_ENV: dev
  COVERAGE: true
  MACOSX_DEPLOYMENT_TARGET: '10.13'
  NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
  PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
  DEPLOYMENT_TYPE: affine

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: ['javascript', 'typescript']
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          # If you wish to specify custom queries, you can do so here or in a config file.
          # By default, queries listed here will override any specified in a config file.
          # Prefix the list here with "+" to use these queries and those in the config file.

          # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
          # queries: security-extended,security-and-quality

      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
      # If this step fails, then you should remove it and run the build manually (see below)
      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      # ℹ️ Command-line programs to run using the OS shell.
      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

      #   If the Autobuild fails above, remove it and uncomment the following three lines.
      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

      # - run: |
      #   echo "Run, Build Application using script"
      #   ./location_of_script_within_repo/buildscript.sh

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
  lint:
    name: Lint
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Run oxlint
        # oxlint is fast, so wrong code will fail quickly
        run: yarn dlx $(node -e "console.log(require('./package.json').scripts['lint:ox'].replace('oxlint', 'oxlint@' + require('./package.json').devDependencies.oxlint))")
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          electron-install: false
          full-cache: true
      - name: Run i18n codegen
        run: yarn i18n-codegen gen
      - name: Run ESLint
        run: yarn lint:eslint --max-warnings=0
      - name: Run Prettier
        # Set nmMode in `actions/setup-node` will modify the .yarnrc.yml
        run: |
          git checkout .yarnrc.yml
          yarn lint:prettier
      - name: Yarn Dedupe
        run: yarn dedupe --check
      - name: Run Type Check
        run: yarn typecheck

  check-yarn-binary:
    name: Check yarn binary
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run check
        run: |
          yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
          git diff --exit-code

  e2e-test:
    name: E2E Test
    runs-on: ubuntu-latest
    env:
      DISTRIBUTION: browser
      IN_CI_TEST: true
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4, 5]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          playwright-install: true
          electron-install: false
          full-cache: true

      - name: Run playwright tests
        run: yarn workspace @affine-test/affine-local e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}

      - name: Upload test results
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: test-results-e2e-${{ matrix.shard }}
          path: ./test-results
          if-no-files-found: ignore

  e2e-migration-test:
    name: E2E Migration Test
    runs-on: ubuntu-latest
    env:
      DISTRIBUTION: browser
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          playwright-install: true
          electron-install: false
          full-cache: true

      - name: Run playwright tests
        run: yarn workspace @affine-test/affine-migration e2e --forbid-only

      - name: Upload test results
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: test-results-e2e-migration
          path: ./tests/affine-migration/test-results
          if-no-files-found: ignore

  unit-test:
    name: Unit Test
    runs-on: ubuntu-latest
    needs:
      - build-native
    env:
      DISTRIBUTION: browser
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          electron-install: false
          full-cache: true

      - name: Download affine.linux-x64-gnu.node
        uses: actions/download-artifact@v4
        with:
          name: affine.linux-x64-gnu.node
          path: ./packages/frontend/native

      - name: Unit Test
        run: yarn nx test:coverage @affine/monorepo

      - name: Upload unit test coverage results
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./.coverage/store/lcov.info
          flags: unittest
          name: affine
          fail_ci_if_error: false

  build-native:
    name: Build AFFiNE native (${{ matrix.spec.target }})
    runs-on: ${{ matrix.spec.os }}
    env:
      CARGO_PROFILE_RELEASE_DEBUG: '1'
    strategy:
      fail-fast: false
      matrix:
        spec:
          - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
          - { os: windows-latest, target: x86_64-pc-windows-msvc }
          - { os: macos-14, target: x86_64-apple-darwin }
          - { os: macos-14, target: aarch64-apple-darwin }

    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          extra-flags: workspaces focus @affine/native
          electron-install: false
      - name: Setup filename
        id: filename
        shell: bash
        run: |
          export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
          echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
      - name: Build AFFiNE native
        uses: ./.github/actions/build-rust
        with:
          target: ${{ matrix.spec.target }}
          package: '@affine/native'
          nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
      - name: Upload ${{ steps.filename.outputs.filename }}
        uses: actions/upload-artifact@v4
        with:
          name: ${{ steps.filename.outputs.filename }}
          path: ./packages/frontend/native/${{ steps.filename.outputs.filename }}
          if-no-files-found: error

  build-server-native:
    name: Build Server native
    runs-on: ubuntu-latest
    env:
      CARGO_PROFILE_RELEASE_DEBUG: '1'
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          extra-flags: workspaces focus @affine/server-native
          electron-install: false
      - name: Build Rust
        uses: ./.github/actions/build-rust
        with:
          target: 'x86_64-unknown-linux-gnu'
          package: '@affine/server-native'
          nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
      - name: Upload server-native.node
        uses: actions/upload-artifact@v4
        with:
          name: server-native.node
          path: ./packages/backend/native/server-native.node
          if-no-files-found: error

  build-web:
    name: Build @affine/web
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          electron-install: false
          full-cache: true
      - name: Build Web
        # always skip cache because its fast, and cache configuration is always changing
        run: yarn nx build @affine/web --skip-nx-cache
        env:
          DISTRIBUTION: 'desktop'
      - name: zip web
        run: tar -czf dist.tar.gz --directory=packages/frontend/electron/renderer/dist .
      - name: Upload web artifact
        uses: actions/upload-artifact@v4
        with:
          name: web
          path: dist.tar.gz
          if-no-files-found: error

  server-test:
    name: Server Test
    runs-on: ubuntu-latest
    needs: build-server-native
    env:
      NODE_ENV: test
      DISTRIBUTION: browser
    services:
      postgres:
        image: postgres
        env:
          POSTGRES_PASSWORD: affine
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      mailer:
        image: mailhog/mailhog
        ports:
          - 1025:1025
          - 8025:8025
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          electron-install: false
          full-cache: true

      - name: Download server-native.node
        uses: actions/download-artifact@v4
        with:
          name: server-native.node
          path: ./packages/backend/server

      - name: Initialize database
        run: |
          psql -h localhost -U postgres -c "CREATE DATABASE affine;"
          psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
          psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
        env:
          PGPASSWORD: affine

      - name: Run init-db script
        run: |
          yarn workspace @affine/server exec prisma generate
          yarn workspace @affine/server exec prisma db push
          yarn workspace @affine/server data-migration run
        env:
          DATABASE_URL: postgresql://affine:affine@localhost:5432/affine

      - name: Run server tests
        run: yarn workspace @affine/server test:coverage
        env:
          CARGO_TARGET_DIR: '${{ github.workspace }}/target'
          DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
          COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'

      - name: Upload server test coverage results
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./packages/backend/server/.coverage/lcov.info
          flags: server-test
          name: affine
          fail_ci_if_error: false

  server-e2e-test:
    name: ${{ matrix.tests.name }}
    runs-on: ubuntu-latest
    env:
      DISTRIBUTION: browser
      DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
      IN_CI_TEST: true
    strategy:
      fail-fast: false
      matrix:
        tests:
          - name: 'Server E2E Test 1/3'
            script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=1/3
          - name: 'Server E2E Test 2/3'
            script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=2/3
          - name: 'Server E2E Test 3/3'
            script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=3/3
          - name: 'Server Desktop E2E Test'
            script: |
              yarn workspace @affine/electron build:dev
              xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop-cloud e2e
    needs:
      - build-server-native
      - build-native
    services:
      postgres:
        image: postgres
        env:
          POSTGRES_PASSWORD: affine
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      mailer:
        image: mailhog/mailhog
        ports:
          - 1025:1025
          - 8025:8025
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        with:
          playwright-install: true
          hard-link-nm: false

      - name: Download server-native.node
        uses: actions/download-artifact@v4
        with:
          name: server-native.node
          path: ./packages/backend/server

      - name: Download affine.linux-x64-gnu.node
        uses: actions/download-artifact@v4
        with:
          name: affine.linux-x64-gnu.node
          path: ./packages/frontend/native

      - name: Initialize database
        run: |
          psql -h localhost -U postgres -c "CREATE DATABASE affine;"
          psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
          psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
        env:
          PGPASSWORD: affine

      - name: Run init-db script
        run: |
          yarn workspace @affine/server exec prisma generate
          yarn workspace @affine/server exec prisma db push
          yarn workspace @affine/server data-migration run

      - name: ${{ matrix.tests.name }}
        run: |
          ${{ matrix.tests.script }}
        env:
          DEV_SERVER_URL: http://localhost:8080

      - name: Upload test results
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: test-results-e2e-server
          path: ./tests/affine-cloud/test-results
          if-no-files-found: ignore

  desktop-test:
    name: Desktop Test (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
    runs-on: ${{ matrix.spec.os }}
    strategy:
      fail-fast: false
      matrix:
        spec:
          - {
              os: macos-14,
              platform: macos,
              arch: x64,
              target: x86_64-apple-darwin,
              test: false,
            }
          - {
              os: macos-14,
              platform: macos,
              arch: arm64,
              target: aarch64-apple-darwin,
              test: true,
            }
          - {
              os: ubuntu-latest,
              platform: linux,
              arch: x64,
              target: x86_64-unknown-linux-gnu,
              test: true,
            }
          - {
              os: windows-latest,
              platform: windows,
              arch: x64,
              target: x86_64-pc-windows-msvc,
              test: true,
            }
    needs:
      - build-web
      - build-native
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: ./.github/actions/setup-node
        timeout-minutes: 10
        with:
          extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
          playwright-install: true
          hard-link-nm: false
          enableScripts: false

      - name: Setup filename
        id: filename
        shell: bash
        run: |
          export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
          echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"

      - name: Download ${{ steps.filename.outputs.filename }}
        uses: actions/download-artifact@v4
        with:
          name: ${{ steps.filename.outputs.filename }}
          path: ./packages/frontend/native

      - name: Run unit tests
        if: ${{ matrix.spec.test }}
        shell: bash
        run: yarn workspace @affine/electron vitest

      - name: Download web artifact
        uses: ./.github/actions/download-web
        with:
          path: packages/frontend/electron/resources/web-static

      - name: Build Desktop Layers
        run: yarn workspace @affine/electron build

      - name: Run desktop tests
        if: ${{ matrix.spec.os == 'ubuntu-latest' }}
        run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e

      - name: Run desktop tests
        if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
        run: yarn workspace @affine-test/affine-desktop e2e

      - name: Make bundle
        if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
        env:
          SKIP_BUNDLE: true
          SKIP_WEB_BUILD: true
          HOIST_NODE_MODULES: 1
        run: yarn workspace @affine/electron package --platform=darwin --arch=arm64

      - name: Make AppImage
        run: yarn workspace @affine/electron make --platform=linux --arch=x64
        if: ${{ matrix.spec.target == 'x86_64-unknown-linux-gnu' }}
        env:
          SKIP_WEB_BUILD: 1
          HOIST_NODE_MODULES: 1

      - name: Output check
        if: ${{ matrix.spec.os == 'macos-14' && matrix.spec.arch == 'arm64' }}
        run: |
          yarn workspace @affine/electron exec node --loader ts-node/esm/transpile-only ./scripts/macos-arm64-output-check.ts

      - name: Upload test results
        if: ${{ failure() }}
        uses: actions/upload-artifact@v4
        with:
          name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
          path: ./test-results
          if-no-files-found: ignore

  test-done:
    needs:
      - analyze
      - lint
      - check-yarn-binary
      - e2e-test
      - e2e-migration-test
      - unit-test
      - server-test
      - server-e2e-test
      - desktop-test
    if: always()
    runs-on: ubuntu-latest
    name: 3, 2, 1 Launch
    steps:
      - run: exit 1
        # Thank you, next https://github.com/vercel/next.js/blob/canary/.github/workflows/build_and_test.yml#L379
        if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
