# This workflow will run in the context of the source of the PR.
# On a PR from a fork, the workflow will not have access to secrets, and so any parts of the build that require secrets will not run.
# If additional artifacts are needed, the failed "build-browser-target.yml" workflow held up by the check-run should be re-run.

name: Build Browser

on:
  pull_request:
    types: [opened, synchronize]
    branches-ignore:
      - 'l10n_master'
      - 'cf-pages'
    paths:
      - 'apps/browser/**'
      - 'libs/**'
      - '*'
      - '!*.md'
      - '!*.txt'
  push:
    branches:
      - 'main'
      - 'rc'
      - 'hotfix-rc-browser'
    paths:
      - 'apps/browser/**'
      - 'libs/**'
      - '*'
      - '!*.md'
      - '!*.txt'
      - '.github/workflows/build-browser.yml'
  workflow_call:
    inputs: {}
  workflow_dispatch:
    inputs:
      sdk_branch:
        description: "Custom SDK branch"
        required: false
        type: string

defaults:
  run:
    shell: bash

permissions:
  contents: read

jobs:
  setup:
    name: Setup
    runs-on: ubuntu-22.04
    outputs:
      repo_url: ${{ steps.gen_vars.outputs.repo_url }}
      adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
      node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
      has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Get Package Version
        id: gen_vars
        run: |
          repo_url=https://github.com/$GITHUB_REPOSITORY.git
          adj_build_num=${GITHUB_SHA:0:7}

          echo "repo_url=$repo_url" >> $GITHUB_OUTPUT
          echo "adj_build_number=$adj_build_num" >> $GITHUB_OUTPUT

      - name: Get Node Version
        id: retrieve-node-version
        working-directory: ./
        run: |
          NODE_NVMRC=$(cat .nvmrc)
          NODE_VERSION=${NODE_NVMRC/v/''}
          echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT

      - name: Check secrets
        id: check-secrets
        run: |
          has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
          echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT


  locales-test:
    name: Locales Test
    runs-on: ubuntu-22.04
    needs:
      - setup
    defaults:
      run:
        working-directory: apps/browser
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Testing locales - extName length
        run: |
         found_error=false

         echo "Locales Test"
         echo "============"
         echo "extName string must be 40 characters or less"
         echo
         for locale in $(ls src/_locales/); do
           string_length=$(jq '.extName.message | length' src/_locales/$locale/messages.json)
           if [[ $string_length -gt 40 ]]; then
             echo "$locale: $string_length"
             found_error=true
           fi
         done

         if $found_error; then
           echo
           echo "Please fix 'extName' for the locales listed above."
           exit 1
         else
           echo "Test passed!"
         fi


  build-source:
    name: Build browser source - ${{matrix.license_type.readable}}
    runs-on: ubuntu-22.04
    needs:
      - setup
      - locales-test
    strategy:
      matrix:
        license_type:
          - include_bitwarden_license_folder: false
            archive_name_prefix: ""
            readable: "open source license"
          - include_bitwarden_license_folder: true
            archive_name_prefix: "bit-"
            readable: "commercial license"
    env:
      _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
      _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Set up Node
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
        with:
          cache: 'npm'
          cache-dependency-path: '**/package-lock.json'
          node-version: ${{ env._NODE_VERSION }}

      - name: Print environment
        run: |
          node --version
          npm --version

      - name: Build sources for reviewers
        run: |
          # Include hidden files in glob copy
          shopt -s dotglob

          # Remove ".git" directory
          rm -r .git

          # Copy root level files to source directory
          mkdir browser-source
          FILES=$(find . -maxdepth 1 -type f)
          for FILE in $FILES; do cp "$FILE" browser-source/; done

          # Copy apps/browser to the Browser source directory
          mkdir -p browser-source/apps/browser
          cp -r apps/browser/* browser-source/apps/browser

          # Copy bitwarden_license/bit-browser to the Browser source directory
          if [[ ${{matrix.license_type.include_bitwarden_license_folder}} == "true" ]]; then
            mkdir -p browser-source/bitwarden_license/bit-browser
            cp -r bitwarden_license/bit-browser/* browser-source/bitwarden_license/bit-browser
          fi

          # Copy libs to Browser source directory
          mkdir browser-source/libs
          cp -r libs/* browser-source/libs

          zip -r browser-source.zip browser-source

      - name: Upload browser source
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: ${{matrix.license_type.archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip
          path: browser-source.zip
          if-no-files-found: error


  build:
    name: Build ${{ matrix.browser.name }} - ${{ matrix.license_type.readable }}
    runs-on: ubuntu-22.04
    needs:
      - setup
      - locales-test
      - build-source
    env:
      _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
      _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
    strategy:
      matrix:
        license_type:
          - build_prefix: ""
            artifact_prefix: ""
            source_archive_name_prefix: ""
            archive_name_prefix: ""
            npm_command_prefix: "dist:"
            readable: "open source license"
          - build_prefix: "bit-"
            artifact_prefix: "bit-"
            source_archive_name_prefix: "bit-"
            archive_name_prefix: "bit-"
            npm_command_prefix: "dist:bit:"
            readable: "commercial license"
        browser:
          - name: "chrome"
            npm_command_suffix: "chrome"
            archive_name: "dist-chrome.zip"
            artifact_name: "dist-chrome-MV3"
          - name: "edge"
            npm_command_suffix: "edge"
            archive_name: "dist-edge.zip"
            artifact_name: "dist-edge-MV3"
          - name: "firefox"
            npm_command_suffix: "firefox"
            archive_name: "dist-firefox.zip"
            artifact_name: "dist-firefox"
          - name: "firefox-mv3"
            npm_command_suffix: "firefox:mv3"
            archive_name: "dist-firefox.zip"
            artifact_name: "DO-NOT-USE-FOR-PROD-dist-firefox-MV3"
          - name: "opera-mv3"
            npm_command_suffix: "opera:mv3"
            archive_name: "dist-opera.zip"
            artifact_name: "dist-opera-MV3"
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Set up Node
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
        with:
          cache: 'npm'
          cache-dependency-path: '**/package-lock.json'
          node-version: ${{ env._NODE_VERSION }}

      - name: Print environment
        run: |
          node --version
          npm --version

      - name: Download browser source
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          name: ${{matrix.license_type.source_archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip

      - name: Unzip browser source artifact
        run: |
          unzip browser-source.zip
          rm browser-source.zip

      - name: NPM setup
        run: npm ci
        working-directory: browser-source/

      - name: Download SDK artifacts
        if: ${{ inputs.sdk_branch != '' }}
        uses: bitwarden/gh-actions/download-artifacts@main
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          workflow: build-wasm-internal.yml
          workflow_conclusion: success
          branch: ${{ inputs.sdk_branch }}
          artifacts: sdk-internal
          repo: bitwarden/sdk-internal
          path: sdk-internal
          if_no_artifact_found: fail

      - name: Override SDK
        if: ${{ inputs.sdk_branch != '' }}
        working-directory: browser-source/
        run: npm link ../sdk-internal

      - name: Check source file size
        if: ${{ startsWith(matrix.browser.name, 'firefox') }}
        run: |
          # Declare variable as indexed array
          declare -a FILES

          # Search for source files that are greater than 5M
          TARGET_DIR='./browser-source/apps/browser'
          while IFS=' ' read -r RESULT; do
              FILES+=("$RESULT")
          done < <(find $TARGET_DIR -size +5M)

          # Validate results and provide messaging
          if [[ ${#FILES[@]} -ne 0 ]]; then
              echo "File(s) exceeds size limit: 5MB"
              for FILE in ${FILES[@]}; do
                  echo "- $(du --si $FILE)"
              done
              echo "ERROR Firefox rejects extension uploads that contain files larger than 5MB"
              # Invoke failure
              exit 1
          fi

      - name: Build extension
        run: npm run ${{matrix.license_type.npm_command_prefix}}${{ matrix.browser.npm_command_suffix }}
        working-directory: browser-source/apps/browser

      - name: Upload extension artifact
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name }}-${{ env._BUILD_NUMBER }}.zip
          path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name }}
          if-no-files-found: error


  build-safari:
    name: Build Safari - ${{ matrix.license_type.readable }}
    runs-on: macos-13
    permissions:
      contents: read
      id-token: write
    needs:
      - setup
      - locales-test
    if: ${{ needs.setup.outputs.has_secrets == 'true' }}
    strategy:
      matrix:
        license_type:
          - build_prefix: ""
            artifact_prefix: ""
            archive_name_prefix: ""
            npm_command_prefix: "dist:"
            readable: "open source license"
          - build_prefix: "bit-"
            artifact_prefix: "bit-"
            archive_name_prefix: "bit-"
            npm_command_prefix: "dist:bit:"
            readable: "commercial license"
    env:
      _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
      _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Set up Node
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
        with:
          cache: 'npm'
          cache-dependency-path: '**/package-lock.json'
          node-version: ${{ env._NODE_VERSION }}

      - name: Print environment
        run: |
          node --version
          npm --version

      - name: Log in to Azure
        uses: bitwarden/gh-actions/azure-login@main
        with:
          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          tenant_id: ${{ secrets.AZURE_TENANT_ID }}
          client_id: ${{ secrets.AZURE_CLIENT_ID }}

      - name: Get Azure Key Vault secrets
        id: get-kv-secrets
        uses: bitwarden/gh-actions/get-keyvault-secrets@main
        with:
          keyvault: gh-clients
          secrets: "KEYCHAIN-PASSWORD"

      - name: Download Provisioning Profiles secrets
        env:
          ACCOUNT_NAME: bitwardenci
          CONTAINER_NAME: profiles
        run: |
          mkdir -p $HOME/secrets

          az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
            --name bitwarden_desktop_appstore.provisionprofile \
            --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \
            --output none

      - name: Get certificates
        run: |
          mkdir -p $HOME/certificates

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key |
            jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert |
            jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert |
            jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert |
            jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert |
            jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12

          az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
            jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12

      - name: Log out from Azure
        uses: bitwarden/gh-actions/azure-logout@main

      - name: Set up keychain
        env:
          KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
        run: |
          security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
          security set-keychain-settings -lut 1200 build.keychain

          security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security import "$HOME/certificates/appstore-app-cert.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
            -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild

          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain

      - name: NPM setup
        run: npm ci
        working-directory: ./

      - name: Download SDK Artifacts
        if: ${{ inputs.sdk_branch != '' }}
        uses: bitwarden/gh-actions/download-artifacts@main
        with:
          github_token: ${{secrets.GITHUB_TOKEN}}
          workflow: build-wasm-internal.yml
          workflow_conclusion: success
          branch: ${{ inputs.sdk_branch }}
          artifacts: sdk-internal
          repo: bitwarden/sdk-internal
          path: ../sdk-internal
          if_no_artifact_found: fail

      - name: Override SDK
        if: ${{ inputs.sdk_branch != '' }}
        working-directory: ./
        run: |
          npm link ../sdk-internal

      - name: Build Safari extension
        run: npm run ${{matrix.license_type.npm_command_prefix}}safari
        working-directory: apps/browser

      - name: Zip Safari build artifact
        run: |
          cd apps/browser/dist
          zip ${{matrix.license_type.archive_name_prefix }}dist-safari.zip ./Safari/**/build/Release/safari.appex -r
          pwd
          ls -la

      - name: Upload Safari artifact
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: ${{matrix.license_type.archive_name_prefix}}dist-safari-${{ env._BUILD_NUMBER }}.zip
          path: apps/browser/dist/${{matrix.license_type.archive_name_prefix}}dist-safari.zip
          if-no-files-found: error

  crowdin-push:
    name: Crowdin Push
    if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-22.04
    permissions:
      contents: write
      pull-requests: write
      id-token: write
    needs:
      - build
      - build-safari
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Log in to Azure
        uses: bitwarden/gh-actions/azure-login@main
        with:
          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          tenant_id: ${{ secrets.AZURE_TENANT_ID }}
          client_id: ${{ secrets.AZURE_CLIENT_ID }}

      - name: Retrieve secrets
        id: retrieve-secrets
        uses: bitwarden/gh-actions/get-keyvault-secrets@main
        with:
          keyvault: "bitwarden-ci"
          secrets: "crowdin-api-token"

      - name: Log out from Azure
        uses: bitwarden/gh-actions/azure-logout@main

      - name: Upload Sources
        uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
          CROWDIN_PROJECT_ID: "268134"
        with:
          config: apps/browser/crowdin.yml
          crowdin_branch_name: main
          upload_sources: true
          upload_translations: false


  check-failures:
    name: Check for failures
    if: always()
    runs-on: ubuntu-22.04
    permissions:
      contents: read
      id-token: write
    needs:
      - setup
      - locales-test
      - build-source
      - build
      - build-safari
      - crowdin-push
    steps:
      - name: Check if any job failed
        if: |
          github.event_name != 'pull_request_target'
          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-browser')
          && contains(needs.*.result, 'failure')
        run: exit 1

      - name: Log in to Azure
        if: failure()
        uses: bitwarden/gh-actions/azure-login@main
        with:
          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          tenant_id: ${{ secrets.AZURE_TENANT_ID }}
          client_id: ${{ secrets.AZURE_CLIENT_ID }}

      - name: Retrieve secrets
        id: retrieve-secrets
        if: failure()
        uses: bitwarden/gh-actions/get-keyvault-secrets@main
        with:
          keyvault: "bitwarden-ci"
          secrets: "devops-alerts-slack-webhook-url"

      - name: Log out from Azure
        if: failure()
        uses: bitwarden/gh-actions/azure-logout@main

      - name: Notify Slack on failure
        uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
        if: failure()
        env:
          SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
        with:
          status: ${{ job.status }}
