name: Build CLI

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

defaults:
  run:
    working-directory: apps/cli

jobs:
  check-run:
    name: Check PR run
    uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main

  setup:
    name: Setup
    runs-on: ubuntu-22.04
    needs:
      - check-run
    outputs:
      package_version: ${{ steps.retrieve-package-version.outputs.package_version }}
      node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
    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: retrieve-package-version
        run: |
          PKG_VERSION=$(jq -r .version package.json)
          echo "package_version=$PKG_VERSION" >> $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

  cli:
    name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }}
    strategy:
      matrix:
        os:
          [
            { base: "linux", distro: "ubuntu-22.04" },
            { base: "mac", distro: "macos-13" }
          ]
        license_type:
          [
            { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
            { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
          ]
    runs-on: ${{ matrix.os.distro }}
    needs:
      - setup
    env:
      _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
      _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
      _WIN_PKG_FETCH_VERSION: 20.11.1
      _WIN_PKG_VERSION: 3.5
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Setup Unix Vars
        run: |
          echo "LOWER_RUNNER_OS=$(echo $RUNNER_OS | awk '{print tolower($0)}')" >> $GITHUB_ENV
          echo "SHORT_RUNNER_OS=$(echo $RUNNER_OS | awk '{print substr($0, 1, 3)}' | \
            awk '{print tolower($0)}')" >> $GITHUB_ENV

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

      - name: Install
        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: |
          ls -l ../
          npm link ../sdk-internal

      - name: Build & Package Unix
        run: npm run dist:${{ matrix.license_type.build_prefix }}:${{ env.SHORT_RUNNER_OS }} --quiet

      - name: Zip Unix
        run: |
          cd ./dist/${{ matrix.license_type.build_prefix }}/${{ env.LOWER_RUNNER_OS }}
          zip ../../bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip ./bw

      - name: Version Test
        run: |
          unzip "./dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip" -d "./test"
          testVersion=$(./test/bw -v)
          echo "version: $_PACKAGE_VERSION"
          echo "testVersion: $testVersion"
          if [[ $testVersion != $_PACKAGE_VERSION ]]; then
            echo "Version test failed."
            exit 1
          fi

      - name: Create checksums Unix
        run: |
          cd ./dist
          shasum -a 256 bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip \
            | awk '{split($0, a); print a[1]}' > bw${{
          matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt

      - name: Upload unix zip asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
          path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
          if-no-files-found: error

      - name: Upload unix checksum asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
          path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
          if-no-files-found: error

  cli-windows:
    name: Windows - ${{ matrix.license_type.readable }}
    strategy:
      matrix:
        license_type:
          [
            { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
            { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
          ]
    runs-on: windows-2022
    needs:
      - setup
    env:
      _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
      _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
      _WIN_PKG_FETCH_VERSION: 20.11.1
      _WIN_PKG_VERSION: 3.5
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Setup Windows builder
        run: |
          choco install checksum --no-progress
          choco install reshack --no-progress
          choco install nasm --no-progress

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

      - name: Get pkg-fetch
        shell: pwsh
        run: |
          cd $HOME
          $fetchedUrl = "https://github.com/yao-pkg/pkg-fetch/releases/download/v$env:_WIN_PKG_VERSION/node-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
          New-Item -ItemType directory -Path .\.pkg-cache
          New-Item -ItemType directory -Path .\.pkg-cache\v$env:_WIN_PKG_VERSION
          Invoke-RestMethod -Uri $fetchedUrl `
            -OutFile ".\.pkg-cache\v$env:_WIN_PKG_VERSION\fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"

      - name: Setup Version Info
        shell: pwsh
        run: |
          $major,$minor,$patch = $env:_PACKAGE_VERSION.split('.')
          $versionInfo = @"
          1 VERSIONINFO
          FILEVERSION $major,$minor,$patch,0
          PRODUCTVERSION $major,$minor,$patch,0
          FILEOS 0x40004
          FILETYPE 0x1
          {
          BLOCK "StringFileInfo"
          {
            BLOCK "040904b0"
            {
              VALUE "CompanyName", "Bitwarden Inc."
              VALUE "ProductName", "Bitwarden"
              VALUE "FileDescription", "Bitwarden CLI"
              VALUE "FileVersion", "$env:_PACKAGE_VERSION"
              VALUE "ProductVersion", "$env:_PACKAGE_VERSION"
              VALUE "OriginalFilename", "bw.exe"
              VALUE "InternalName", "bw"
              VALUE "LegalCopyright", "Copyright Bitwarden Inc."
            }
          }
          BLOCK "VarFileInfo"
          {
            VALUE "Translation", 0x0409 0x04B0
          }
          }
          "@
          $versionInfo | Out-File ./version-info.rc
      # https://github.com/vercel/pkg-fetch/issues/188

      - name: Resource Hacker
        shell: cmd
        run: |
          set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
          set WIN_PKG=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\fetched-v%_WIN_PKG_FETCH_VERSION%-win-x64
          set WIN_PKG_BUILT=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\built-v%_WIN_PKG_FETCH_VERSION%-win-x64
          copy %WIN_PKG% %WIN_PKG_BUILT%
          ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action delete -mask ICONGROUP,1,
          ResourceHacker -open version-info.rc -save version-info.res -action compile
          ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action addoverwrite -resource version-info.res

      - name: Install
        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: |
          ls -l ../
          npm link ../sdk-internal

      - name: Build & Package Windows
        run: npm run dist:${{ matrix.license_type.build_prefix }}:win --quiet

      - name: Package Chocolatey
        shell: pwsh
        if: ${{ matrix.license_type.build_prefix == 'bit' }}
        run: |
          Copy-Item -Path stores/chocolatey -Destination dist/chocolatey -Recurse
          Copy-Item dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe -Destination dist/chocolatey/tools
          Copy-Item ${{ github.workspace }}/LICENSE.txt -Destination dist/chocolatey/tools
          choco pack dist/chocolatey/bitwarden-cli.nuspec --version ${{ env._PACKAGE_VERSION }} --out dist/chocolatey

      - name: Zip Windows
        shell: cmd
        run: 7z a ./dist/bw${{ matrix.license_type.artifact_prefix}}-windows-%_PACKAGE_VERSION%.zip ./dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe

      - name: Version Test
        run: |
          dir ./dist/
          Expand-Archive -Path "./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/${{ matrix.license_type.build_prefix }}/windows"
          $testVersion = Invoke-Expression '& ./test/${{ matrix.license_type.build_prefix }}/windows/bw.exe -v'
          echo "version: $env:_PACKAGE_VERSION"
          echo "testVersion: $testVersion"
          if($testVersion -ne $env:_PACKAGE_VERSION) {
            Throw "Version test failed."
          }

      - name: Create checksums Windows
        run: |
          checksum -f="./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${env:_PACKAGE_VERSION}.zip" `
            -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt

      - name: Upload windows zip asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
          path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
          if-no-files-found: error

      - name: Upload windows checksum asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
          path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
          if-no-files-found: error

      - name: Upload Chocolatey asset
        if: matrix.license_type.build_prefix == 'bit'
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
          path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
          if-no-files-found: error

      - name: Zip NPM Build Artifact
        run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath .\bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip

      - name: Upload NPM Build Directory asset
        if: matrix.license_type.build_prefix == 'bit'
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
          path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
          if-no-files-found: error

  snap:
    name: Build Snap
    # Note, before updating the ubuntu version of the workflow, ensure the snap base image
    # is equal or greater than the new version. Otherwise there might be GLIBC version issues.
    # The snap base for CLI is defined in `apps/cli/stores/snap/snapcraft.yaml`
    runs-on: ubuntu-22.04
    needs: [setup, cli]
    env:
      _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
    steps:
      - name: Check out repo
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{  github.event.pull_request.head.sha }}

      - name: Print environment
        run: |
          whoami
          echo "GitHub ref: $GITHUB_REF"
          echo "GitHub event: $GITHUB_EVENT"
          echo "BW Package Version: $_PACKAGE_VERSION"

      - name: Get bw linux cli
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: bw-linux-${{ env._PACKAGE_VERSION }}.zip
          path: apps/cli/dist/snap

      - name: Setup Snap Package
        run: |
          cp -r stores/snap/* -t dist/snap
          sed -i s/__version__/${{ env._PACKAGE_VERSION }}/g dist/snap/snapcraft.yaml
          cd dist/snap
          ls -alth

      - name: Build snap
        uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
        with:
          path: apps/cli/dist/snap

      - name: Create checksum
        run: |
          cd dist/snap
          ls -alth
          sha256sum bw_${{ env._PACKAGE_VERSION }}_amd64.snap \
            | awk '{split($0, a); print a[1]}' > bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt

      - name: Install Snap
        run: sudo snap install dist/snap/bw*.snap --dangerous

      - name: Test Snap
        shell: pwsh
        run: |
          $testVersion = Invoke-Expression '& bw -v'
          if($testVersion -ne $env:_PACKAGE_VERSION) {
            Throw "Version test failed."
          }
        env:
          BITWARDENCLI_APPDATA_DIR: "/home/runner/snap/bw/x1/.config/Bitwarden CLI"

      - name: Cleanup Test & Update Snap for Publish
        shell: pwsh
        run: sudo snap remove bw

      - name: Upload snap asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
          path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
          if-no-files-found: error

      - name: Upload snap checksum asset
        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
        with:
          name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
          path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
          if-no-files-found: error


  check-failures:
    name: Check for failures
    if: always()
    runs-on: ubuntu-22.04
    needs:
      - setup
      - cli
      - cli-windows
      - snap
    steps:
      - name: Check if any job failed
        working-directory: ${{ github.workspace }}
        if: |
          github.event_name != 'pull_request_target'
          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-cli')
          && contains(needs.*.result, 'failure')
        run: exit 1

      - name: Login to Azure - Prod Subscription
        uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
        if: failure()
        with:
          creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

      - 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: 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 }}
