# 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-web-target.yml" workflow held up by the check-run should be re-run. name: Build Web on: pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' paths: - 'apps/web/**' - 'bitwarden_license/bit-common/**' - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-web.yml' push: branches: - 'main' - 'rc' - 'hotfix-rc-web' paths: - 'apps/web/**' - 'bitwarden_license/bit-common/**' - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-web.yml' release: types: [published] workflow_call: inputs: {} workflow_dispatch: inputs: custom_tag_extension: description: "Custom image tag extension" required: false sdk_branch: description: "Custom SDK branch" required: false type: string env: _AZ_REGISTRY: bitwardenprod.azurecr.io _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} permissions: contents: read jobs: setup: name: Setup runs-on: ubuntu-24.04 outputs: version: ${{ steps.version.outputs.value }} 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 GitHub sha as version id: version run: echo "value=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT - name: Get Node Version id: retrieve-node-version 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 build-containers: name: "Build [${{matrix.artifact_name}}], image tag: [${{matrix.image_name}}]" runs-on: ubuntu-24.04 permissions: security-events: write id-token: write needs: setup strategy: fail-fast: false matrix: include: - artifact_name: selfhosted-open-source image_name: web-oss npm_command: dist:oss:selfhost - artifact_name: cloud-COMMERCIAL image_name: web-cloud npm_command: dist:bit:cloud - artifact_name: selfhosted-COMMERCIAL image_name: web npm_command: dist:bit:selfhost - artifact_name: cloud-QA image_name: web-qa-cloud npm_command: build:bit:qa git_metadata: true - artifact_name: ee image_name: web-ee npm_command: build:bit:ee git_metadata: true - artifact_name: cloud-euprd image_name: web-euprd npm_command: build:bit:euprd - artifact_name: cloud-euqa image_name: web-euqa npm_command: build:bit:euqa git_metadata: true - artifact_name: cloud-usdev image_name: web-usdev npm_command: build:bit:usdev git_metadata: true env: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} _VERSION: ${{ needs.setup.outputs.version }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} - name: Get Latest Server Version id: latest-server-version uses: bitwarden/gh-actions/get-release-version@main with: repository: bitwarden/server trim: false - name: Set Server Ref id: set-server-ref run: | SERVER_REF="${{ steps.latest-server-version.outputs.version }}" echo "Latest server release version: $SERVER_REF" if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then SERVER_REF="$GITHUB_REF" elif [[ "$GITHUB_REF" == "refs/heads/rc" ]]; then SERVER_REF="$GITHUB_REF" elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then SERVER_REF="refs/heads/main" fi echo "Server ref: $SERVER_REF" echo "server_ref=$SERVER_REF" >> $GITHUB_OUTPUT - name: Check out Server repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: server repository: bitwarden/server ref: ${{ steps.set-server-ref.outputs.server_ref }} - name: Check Branch to Publish env: PUBLISH_BRANCHES: "main,rc,hotfix-rc-web" id: publish-branch-check run: | IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then echo "is_publish_branch=true" >> $GITHUB_ENV else echo "is_publish_branch=false" >> $GITHUB_ENV fi - name: Add Git metadata to build version working-directory: apps/web if: matrix.git_metadata run: | VERSION=$( jq -r ".version" package.json) jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp mv package.json.tmp package.json ########## Set up Docker ########## - name: Set up Docker uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0 with: daemon-config: | { "debug": true, "features": { "containerd-snapshotter": true } } - name: Set up QEMU emulators uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 ########## ACRs ########## - name: Log in to Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} 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: Log into Prod container registry if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: az acr login -n ${_AZ_REGISTRY%.azurecr.io} ########## Generate image tag and build Docker image ########## - name: Generate container image tag id: tag run: | if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF_NAME}" | sed "s#/#-#g") fi if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags fi if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }} if [[ $TAG_EXTENSION ]]; then IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION fi echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT ########## Build Image ########## - name: Generate image full name id: image-name env: IMAGE_TAG: ${{ steps.tag.outputs.image_tag }} PROJECT_NAME: ${{ matrix.image_name }} run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image id: build-container uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: build-args: | NODE_VERSION=${{ env._NODE_VERSION }} NPM_COMMAND=${{ matrix.npm_command }} context: . file: apps/web/Dockerfile load: true platforms: | linux/amd64, linux/arm/v7, linux/arm64 push: false tags: ${{ steps.image-name.outputs.name }} - name: Push images if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: IMAGE_NAME: ${{ steps.image-name.outputs.name }} run: docker push $IMAGE_NAME - name: Zip project working-directory: apps/web env: IMAGE_NAME: ${{ steps.image-name.outputs.name }} run: | mkdir build docker run --rm --volume $(pwd)/build:/temp --entrypoint sh \ $IMAGE_NAME -c "cp -r ./ /temp" zip -r web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip build - name: Upload ${{ matrix.artifact_name }} artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip if-no-files-found: error - name: Install Cosign if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - name: Sign image with Cosign if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' env: DIGEST: ${{ steps.build-container.outputs.digest }} TAGS: ${{ steps.image-name.outputs.name }} run: | IFS="," read -a tags <<< "${TAGS}" images="" for tag in "${tags[@]}"; do images+="${tag}@${DIGEST} " done cosign sign --yes ${images} - name: Scan Docker image if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: container-scan uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0 with: image: ${{ steps.image-name.outputs.name }} fail-build: false output-format: sarif - name: Upload Grype results to GitHub if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - name: Log out of Docker run: docker logout $_AZ_REGISTRY - name: Log out from Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/azure-logout@main crowdin-push: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' needs: build-containers permissions: contents: write pull-requests: write id-token: write runs-on: ubuntu-24.04 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: "308189" with: config: apps/web/crowdin.yml crowdin_branch_name: main upload_sources: true upload_translations: false trigger-web-vault-deploy: name: Trigger web vault deploy if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 needs: build-containers permissions: id-token: write steps: - 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 github PAT secrets id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main - name: Trigger web vault deploy using GitHub Run ID uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} script: | await github.rest.actions.createWorkflowDispatch({ owner: 'bitwarden', repo: 'clients', workflow_id: 'deploy-web.yml', ref: 'main', inputs: { 'environment': 'USDEV', 'build-web-run-id': '${{ github.run_id }}' } }) check-failures: name: Check for failures if: always() runs-on: ubuntu-24.04 needs: - setup - build-containers - crowdin-push - trigger-web-vault-deploy permissions: id-token: write 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-web') && contains(needs.*.result, 'failure') run: exit 1 - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main if: failure() 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 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 }}