diff --git a/.github/actions/mend-sast/action.yml b/.github/actions/mend-sast/action.yml
new file mode 100644
index 0000000000..8238c5e032
--- /dev/null
+++ b/.github/actions/mend-sast/action.yml
@@ -0,0 +1,185 @@
+name: 'Mend SAST Scan'
+description: 'SAST scan packages for release'
+inputs:
+ scan_dir:
+ description: 'Directory to scan'
+ required: false
+ default: 'packages/'
+ full_scan:
+ description: 'Scan full repo if true'
+ required: false
+ default: 'false'
+ MEND_SAST_REPORT_NAME:
+ description: 'Filename for the output of the scan'
+ required: false
+ default: 'Unified_JS_SAST_Scan_Report'
+ MEND_EMAIL:
+ required: true
+ MEND_USER_KEY:
+ required: true
+ MEND_SERVER_URL:
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Mend SAST scan
+ env:
+ SCAN_DIR: ${{ inputs.scan_dir }}
+ FULL_SCAN: ${{ inputs.full_scan }}
+ REPORT_NAME: ${{ inputs.MEND_SAST_REPORT_NAME }}
+ MEND_EMAIL: ${{ inputs.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ inputs.MEND_USER_KEY }}
+ MEND_URL: ${{ inputs.MEND_SERVER_URL }}
+ MEND_SAST_PATH_EXCLUSIONS: '/node_modules/,/dist/,/coverage/,/out-tsc/,/temp/'
+ shell: bash
+ run: |
+ args=(--scope 'Mobile//unified-sdk-javascript' --retries 2 --non-interactive --report --filename "$REPORT_NAME" --formats json --name "${REPORT_NAME}_$(date +%s)")
+ # Omit `--dir` on a full scan so Mend scans the whole repo.
+ if [ "$FULL_SCAN" != "true" ]; then
+ args+=(--dir "$SCAN_DIR")
+ fi
+ mend code "${args[@]}" > "$REPORT_NAME.txt"
+
+ - name: Extract Scan Results
+ shell: bash
+ run: |
+ export MEND_SAST_TOTAL_VULNERABILITIES_COUNT=$(jq '.[0].stats.totalVulnerabilities' ${{ inputs.MEND_SAST_REPORT_NAME }}.json)
+ export MEND_SAST_CRITICAL_COUNT=$(jq '.[0].stats.critical' ${{ inputs.MEND_SAST_REPORT_NAME }}.json)
+ export MEND_SAST_HIGH_COUNT=$(jq '.[0].stats.high' ${{ inputs.MEND_SAST_REPORT_NAME }}.json)
+ export MEND_SAST_MEDIUM_COUNT=$(jq '.[0].stats.medium' ${{ inputs.MEND_SAST_REPORT_NAME }}.json)
+ export MEND_SAST_LOW_COUNT=$(jq '.[0].stats.low' ${{ inputs.MEND_SAST_REPORT_NAME }}.json)
+ export MEND_SAST_SCAN_URL=$(grep -Eo '(http|https)://saas\.whitesourcesoftware\.com[^ ]+' ${{ inputs.MEND_SAST_REPORT_NAME }}.txt | head -n 1)
+ echo "MEND_SAST_TOTAL_VULNERABILITIES_COUNT=$MEND_SAST_TOTAL_VULNERABILITIES_COUNT" >> $GITHUB_ENV
+ echo "MEND_SAST_CRITICAL_COUNT=$MEND_SAST_CRITICAL_COUNT" >> $GITHUB_ENV
+ echo "MEND_SAST_HIGH_COUNT=$MEND_SAST_HIGH_COUNT" >> $GITHUB_ENV
+ echo "MEND_SAST_MEDIUM_COUNT=$MEND_SAST_MEDIUM_COUNT" >> $GITHUB_ENV
+ echo "MEND_SAST_LOW_COUNT=$MEND_SAST_LOW_COUNT" >> $GITHUB_ENV
+ echo "MEND_SAST_SCAN_URL=$MEND_SAST_SCAN_URL" >> $GITHUB_ENV
+
+ - name: Show the SAST Report in Step Summary
+ shell: bash
+ run: |
+ # Determine badge color based on severity
+ if [ "$MEND_SAST_CRITICAL_COUNT" -gt 0 ]; then
+ BADGE_COLOR="red"
+ BADGE_TEXT="${MEND_SAST_CRITICAL_COUNT}_critical"
+ elif [ "$MEND_SAST_HIGH_COUNT" -gt 0 ]; then
+ BADGE_COLOR="orange"
+ BADGE_TEXT="${MEND_SAST_HIGH_COUNT}_high"
+ elif [ "$MEND_SAST_MEDIUM_COUNT" -gt 0 ]; then
+ BADGE_COLOR="yellow"
+ BADGE_TEXT="${MEND_SAST_MEDIUM_COUNT}_medium"
+ elif [ "$MEND_SAST_LOW_COUNT" -gt 0 ]; then
+ BADGE_COLOR="blue"
+ BADGE_TEXT="${MEND_SAST_LOW_COUNT}_low"
+ else
+ BADGE_COLOR="brightgreen"
+ BADGE_TEXT="clean"
+ fi
+
+ BADGE_URL="https://img.shields.io/badge/SAST-${BADGE_TEXT}-${BADGE_COLOR}"
+
+ echo "## Mend SAST Report " >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Add a table summarizing results
+ echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
+ echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| 🔴 Critical | $MEND_SAST_CRITICAL_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🟠High | $MEND_SAST_HIGH_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🟡 Medium | $MEND_SAST_MEDIUM_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🔵 Low | $MEND_SAST_LOW_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Add Mend report link if available
+ if [ -n "$MEND_SAST_SCAN_URL" ]; then
+ echo "**🔗 [View Full Mend SAST Report]($MEND_SAST_SCAN_URL)**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # Add expandable section with raw scan output
+ echo "Expand for details
" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ cat ${{ inputs.MEND_SAST_REPORT_NAME }}.txt >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
+
+ # Step 1: Decide Check Conclusion
+ # - name: Determine Mend SAST Check Conclusion
+ # id: sast_conclusion
+ # shell: bash
+ # run: |
+ # if [ "$MEND_SAST_CRITICAL_COUNT" -gt 0 ] || [ "$MEND_SAST_HIGH_COUNT" -gt 0 ]; then
+ # echo "conclusion=failure" >> $GITHUB_OUTPUT
+ # else
+ # echo "conclusion=success" >> $GITHUB_OUTPUT
+ # fi
+
+ # Step 2: Publish Mend SAST Check
+ # - name: Publish Mend SAST Check
+ # uses: actions/github-script@v9
+ # env:
+ # REPORT_NAME: ${{ inputs.MEND_SAST_REPORT_NAME }}
+ # with:
+ # script: |
+ # const fs = require('fs');
+ # const body = fs.readFileSync(process.env.REPORT_NAME + '.txt', 'utf8');
+
+ # await github.rest.checks.create({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # name: "Mend SAST Report",
+ # head_sha: '${{ inputs.head_sha || github.event.pull_request.head.sha || github.sha }}',
+ # status: "completed",
+ # conclusion: "${{ steps.sast_conclusion.outputs.conclusion }}",
+ # output: {
+ # title: "Mend SAST Report",
+ # summary: `Critical: ${process.env.MEND_SAST_CRITICAL_COUNT}, High: ${process.env.MEND_SAST_HIGH_COUNT}`,
+ # text: "```\n" + body + "\n```"
+ # }
+ # });
+
+ - name: Upload SAST scan report
+ uses: actions/upload-artifact@v7
+ with:
+ name: mend-sast-report
+ path: |
+ ${{ inputs.MEND_SAST_REPORT_NAME }}.txt
+ ${{ inputs.MEND_SAST_REPORT_NAME }}.json
+
+ # - name: Send slack notification
+ # uses: 8398a7/action-slack@v3
+ # if: always()
+ # with:
+ # status: custom
+ # fields: all
+ # custom_payload: |
+ # {
+ # "text": "*Mend SAST Scan Results*",
+ # "attachments": [
+ # {
+ # "color": "${{ (env.MEND_SAST_CRITICAL_COUNT == '0' && env.MEND_SAST_HIGH_COUNT == '0') && 'good' || 'danger' }}",
+ # "fields": [
+ # {
+ # "title": "SAST scan",
+ # "value": "Total: ${{ env.MEND_SAST_TOTAL_VULNERABILITIES_COUNT }} | Critical: ${{ env.MEND_SAST_CRITICAL_COUNT }} | High: ${{ env.MEND_SAST_HIGH_COUNT }} | Medium: ${{ env.MEND_SAST_MEDIUM_COUNT }} | Low: ${{ env.MEND_SAST_LOW_COUNT }}\n<${{ env.MEND_SAST_SCAN_URL }}|View full SAST report>",
+ # "short": false
+ # }
+ # ]
+ # }
+ # ]
+ # }
+ # env:
+ # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+
+ # Fail the job on Critical/High findings
+ - name: Fail on Critical/High severity vulnerabilities
+ shell: bash
+ run: |
+ if [ "$MEND_SAST_CRITICAL_COUNT" -gt 0 ] || [ "$MEND_SAST_HIGH_COUNT" -gt 0 ]; then
+ echo "::error::Mend SAST scan found ${MEND_SAST_CRITICAL_COUNT} critical and ${MEND_SAST_HIGH_COUNT} high severity vulnerabilities. See the Mend SAST Report step summary for details."
+ exit 1
+ fi
+ echo "No critical or high severity vulnerabilities found."
diff --git a/.github/actions/mend-sca/action.yml b/.github/actions/mend-sca/action.yml
new file mode 100644
index 0000000000..529350f80b
--- /dev/null
+++ b/.github/actions/mend-sca/action.yml
@@ -0,0 +1,225 @@
+name: 'Mend SCA Scan'
+description: 'SCA scan packages for release'
+inputs:
+ scan_dir:
+ description: 'Directory to scan'
+ required: false
+ default: 'packages/'
+ full_scan:
+ description: 'Scan full repo if true'
+ required: false
+ default: 'false'
+ MEND_SCA_REPORT_NAME:
+ description: 'Filename for the output of the scan'
+ required: false
+ default: 'Unified_JS_SCA_Scan_Report'
+ MEND_EMAIL:
+ required: true
+ MEND_USER_KEY:
+ required: true
+ MEND_SERVER_URL:
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - uses: pnpm/action-setup@v4
+ with:
+ run_install: false
+
+ - name: Setup pnpm config
+ run: pnpm config set store-dir '.pnpm-store' --global
+ shell: bash
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version-file: '.node-version'
+ cache: 'pnpm'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+ shell: bash
+
+ - name: Mend SCA scan
+ env:
+ SCAN_DIR: ${{ inputs.scan_dir }}
+ FULL_SCAN: ${{ inputs.full_scan }}
+ REPORT_NAME: ${{ inputs.MEND_SCA_REPORT_NAME }}
+ MEND_EMAIL: ${{ inputs.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ inputs.MEND_USER_KEY }}
+ MEND_URL: ${{ inputs.MEND_SERVER_URL }}
+ shell: bash
+ # Mend SCA scan does not generate a report so tee output to both console and file
+ # --update flag syncs the results to the Mend portal and overwrites any pre-existing vulnerabilities
+ run: |
+ args=(--non-interactive --update --scope 'Mobile//unified-sdk-javascript')
+ # Omit `--dir` on a full scan so Mend scans the whole repo.
+ if [ "$FULL_SCAN" != "true" ]; then
+ args+=(--dir "$SCAN_DIR")
+ fi
+ mend dep "${args[@]}" 2>&1 | tee "$REPORT_NAME.txt"
+
+ - name: Extract Scan Results
+ shell: bash
+ run: |
+ # Extract scan results with more robust parsing
+ MEND_SCA_SCAN_URL=$(grep -Eo '(http|https)://saas\.whitesourcesoftware\.com[^ ]+' ${{ inputs.MEND_SCA_REPORT_NAME }}.txt | head -n 1)
+ MEND_SCA_SCAN_SUMMARY=$(grep -E "Detected.*vulnerabilities" ${{ inputs.MEND_SCA_REPORT_NAME }}.txt || echo 'No vulnerabilities detected')
+
+ # Initialize counts with default value of 0
+ MEND_SCA_CRITICAL_COUNT=0
+ MEND_SCA_HIGH_COUNT=0
+ MEND_SCA_MEDIUM_COUNT=0
+ MEND_SCA_LOW_COUNT=0
+
+ # Extract vulnerability counts with error handling
+ if grep -q "Critical" ${{ inputs.MEND_SCA_REPORT_NAME }}.txt; then
+ MEND_SCA_CRITICAL_COUNT=$(grep -Eo '[0-9]+ Critical' ${{ inputs.MEND_SCA_REPORT_NAME }}.txt | awk '{print $1}')
+ fi
+ if grep -q "High" ${{ inputs.MEND_SCA_REPORT_NAME }}.txt; then
+ MEND_SCA_HIGH_COUNT=$(grep -Eo '[0-9]+ High' ${{ inputs.MEND_SCA_REPORT_NAME }}.txt | awk '{print $1}')
+ fi
+ if grep -q "Medium" ${{ inputs.MEND_SCA_REPORT_NAME }}.txt; then
+ MEND_SCA_MEDIUM_COUNT=$(grep -Eo '[0-9]+ Medium' ${{ inputs.MEND_SCA_REPORT_NAME }}.txt | awk '{print $1}')
+ fi
+ if grep -q "Low" ${{ inputs.MEND_SCA_REPORT_NAME }}.txt; then
+ MEND_SCA_LOW_COUNT=$(grep -Eo '[0-9]+ Low' ${{ inputs.MEND_SCA_REPORT_NAME }}.txt | awk '{print $1}')
+ fi
+
+ # Set environment variables with error checking
+ if [ ! -z "$MEND_SCA_SCAN_URL" ]; then
+ echo "MEND_SCA_SCAN_URL=$MEND_SCA_SCAN_URL" >> $GITHUB_ENV
+ fi
+ echo "MEND_SCA_SCAN_SUMMARY=$MEND_SCA_SCAN_SUMMARY" >> $GITHUB_ENV
+ echo "MEND_SCA_CRITICAL_COUNT=$MEND_SCA_CRITICAL_COUNT" >> $GITHUB_ENV
+ echo "MEND_SCA_HIGH_COUNT=$MEND_SCA_HIGH_COUNT" >> $GITHUB_ENV
+ echo "MEND_SCA_MEDIUM_COUNT=$MEND_SCA_MEDIUM_COUNT" >> $GITHUB_ENV
+ echo "MEND_SCA_LOW_COUNT=$MEND_SCA_LOW_COUNT" >> $GITHUB_ENV
+
+ - name: Show the SCA Report in Step Summary
+ shell: bash
+ run: |
+ # Pick badge color based on severity counts
+ if [ "$MEND_SCA_CRITICAL_COUNT" -gt 0 ]; then
+ BADGE_COLOR="red"
+ BADGE_TEXT="${MEND_SCA_CRITICAL_COUNT}_critical"
+ elif [ "$MEND_SCA_HIGH_COUNT" -gt 0 ]; then
+ BADGE_COLOR="orange"
+ BADGE_TEXT="${MEND_SCA_HIGH_COUNT}_high"
+ elif [ "$MEND_SCA_MEDIUM_COUNT" -gt 0 ]; then
+ BADGE_COLOR="yellow"
+ BADGE_TEXT="${MEND_SCA_MEDIUM_COUNT}_medium"
+ elif [ "$MEND_SCA_LOW_COUNT" -gt 0 ]; then
+ BADGE_COLOR="blue"
+ BADGE_TEXT="${MEND_SCA_LOW_COUNT}_low"
+ else
+ BADGE_COLOR="brightgreen"
+ BADGE_TEXT="clean"
+ fi
+
+ BADGE_URL="https://img.shields.io/badge/SCA-${BADGE_TEXT}-${BADGE_COLOR}"
+
+ echo "## Mend SCA Report " >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Show counts in a table
+ echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
+ echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| 🔴 Critical | $MEND_SCA_CRITICAL_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🟠High | $MEND_SCA_HIGH_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🟡 Medium | $MEND_SCA_MEDIUM_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "| 🔵 Low | $MEND_SCA_LOW_COUNT |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # Add Mend report link if available
+ if [ -n "$MEND_SCA_SCAN_URL" ]; then
+ echo "**🔗 [View Full Mend SCA Report]($MEND_SCA_SCAN_URL)**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # Expandable section for full output
+ echo "Expand for details
" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ cat ${{ inputs.MEND_SCA_REPORT_NAME }}.txt >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
+
+ # # Step 1: Decide Check Conclusion
+ # - name: Determine Mend SCA Check Conclusion
+ # id: sca_conclusion
+ # shell: bash
+ # run: |
+ # if [ "$MEND_SCA_CRITICAL_COUNT" -gt 0 ] || [ "$MEND_SCA_HIGH_COUNT" -gt 0 ]; then
+ # echo "conclusion=failure" >> $GITHUB_OUTPUT
+ # else
+ # echo "conclusion=success" >> $GITHUB_OUTPUT
+ # fi
+
+ # # Step 2: Publish Mend SCA Check
+ # - name: Publish Mend SCA Check
+ # uses: actions/github-script@v9
+ # env:
+ # REPORT_NAME: ${{ inputs.MEND_SCA_REPORT_NAME }}
+ # with:
+ # script: |
+ # const fs = require('fs');
+ # const body = fs.readFileSync(process.env.REPORT_NAME + '.txt', 'utf8');
+
+ # await github.rest.checks.create({
+ # owner: context.repo.owner,
+ # repo: context.repo.repo,
+ # name: "Mend SCA Report",
+ # head_sha: '${{ inputs.head_sha || github.event.pull_request.head.sha || github.sha }}',
+ # status: "completed",
+ # conclusion: "${{ steps.sca_conclusion.outputs.conclusion }}",
+ # output: {
+ # title: "Mend SCA Report",
+ # summary: `Critical: ${process.env.MEND_SCA_CRITICAL_COUNT}, High: ${process.env.MEND_SCA_HIGH_COUNT}`,
+ # text: "```\n" + body + "\n```"
+ # }
+ # });
+
+ - name: Upload SCA scan report
+ uses: actions/upload-artifact@v7
+ with:
+ name: mend-sca-report
+ path: ${{ inputs.MEND_SCA_REPORT_NAME }}.txt
+
+ # - name: Send slack notification
+ # uses: 8398a7/action-slack@v3
+ # if: always()
+ # with:
+ # status: custom
+ # fields: all
+ # custom_payload: |
+ # {
+ # "text": "*Mend SCA Scan Results*",
+ # "attachments": [
+ # {
+ # "color": "${{ (env.MEND_SCA_CRITICAL_COUNT == '0' && env.MEND_SCA_HIGH_COUNT == '0') && 'good' || 'danger' }}",
+ # "fields": [
+ # {
+ # "title": "SCA scan",
+ # "value": "${{ env.MEND_SCA_SCAN_SUMMARY }}\n<${{ env.MEND_SCA_SCAN_URL }}|View full SCA report>",
+ # "short": false
+ # }
+ # ]
+ # }
+ # ]
+ # }
+ # env:
+ # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+
+ # Fail the job (not just the informational Check above) so that
+ # ci.yaml's `needs:` gating and branch protection actually block on
+ # Critical/High findings instead of showing green.
+ - name: Fail on Critical/High severity vulnerabilities
+ shell: bash
+ run: |
+ if [ "$MEND_SCA_CRITICAL_COUNT" -gt 0 ] || [ "$MEND_SCA_HIGH_COUNT" -gt 0 ]; then
+ echo "::error::Mend SCA scan found ${MEND_SCA_CRITICAL_COUNT} critical and ${MEND_SCA_HIGH_COUNT} high severity vulnerabilities. See the Mend SCA Report step summary for details."
+ exit 1
+ fi
+ echo "No critical or high severity vulnerabilities found."
diff --git a/.github/actions/mend-setup/action.yml b/.github/actions/mend-setup/action.yml
new file mode 100644
index 0000000000..cc331d9277
--- /dev/null
+++ b/.github/actions/mend-setup/action.yml
@@ -0,0 +1,43 @@
+name: 'Mend Scan Setup'
+description: 'Setup a Mend scan'
+
+runs:
+ using: 'composite'
+ steps:
+ # Fetch just the checksum first (a few bytes) so the cache key reflects the
+ # CLI version Mend is currently serving.
+ - name: Fetch Mend CLI checksum
+ shell: bash
+ run: curl -fsSL https://downloads.mend.io/cli/linux_amd64/mend.sha256 -o mend.sha256
+
+ - name: Restore Cached Mend CLI
+ id: mend-cache
+ uses: actions/cache@v6
+ env:
+ mend-cache-name: mend-cli
+ with:
+ path: ${{ github.workspace }}/bin/mend
+ key: ${{ runner.os }}-${{ env.mend-cache-name }}-${{ hashFiles('mend.sha256') }}
+
+ - if: ${{ steps.mend-cache.outputs.cache-hit != 'true' }}
+ name: Download Mend CLI executable (cache miss...)
+ shell: bash
+ # Download Mend CLI if it passes integrity check
+ run: |
+ mkdir -p ${{ github.workspace }}/bin
+ curl -fsSL https://downloads.mend.io/cli/linux_amd64/mend -o ${{ github.workspace }}/bin/mend
+ expected="$(awk '{print $1}' mend.sha256)"
+ actual="$(sha256sum "${{ github.workspace }}/bin/mend" | awk '{print $1}')"
+ if [ "${actual}" != "${expected}" ]; then
+ echo "::error::Mend CLI checksum mismatch: expected ${expected}, got ${actual}"
+ exit 1
+ fi
+ chmod +x ${{ github.workspace }}/bin/mend
+
+ - name: Add Mend CLI to Path
+ shell: bash
+ run: echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
+
+ - name: Test Mend CLI
+ shell: bash
+ run: mend version
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 9d83145428..90562c02e5 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -49,8 +49,14 @@ runs:
${{ runner.os }}-playwright-
- name: Install Playwright
- run: pnpm exec playwright install
shell: bash
+ # Playwright fails to install Chromium on Ubuntu 26.04 x64 because its built-in OS-to-platform mapping
+ # table does not yet recognize the new operating system version, triggering an ERROR: Playwright does
+ # not support chromium on ubuntu26.04-x64 message.
+ # https://github.com/microsoft/playwright/issues/40117
+ run: |
+ export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE="ubuntu24.04-x64"
+ pnpm exec playwright install
- name: Set Nx SHAs
uses: nrwl/nx-set-shas@v4
diff --git a/.github/workflows/mend-release-scan.yml b/.github/workflows/mend-release-scan.yml
new file mode 100644
index 0000000000..d7cb94edf3
--- /dev/null
+++ b/.github/workflows/mend-release-scan.yml
@@ -0,0 +1,85 @@
+# Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
+#
+# This software may be modified and distributed under the terms
+# of the MIT license. See the LICENSE file for details.
+#
+
+name: Mend Scans for Release
+on:
+ workflow_dispatch:
+ inputs:
+ scan_dir:
+ description: 'Directory to scan'
+ required: false
+ default: 'packages/'
+ type: string
+ full_scan:
+ description: 'Scan full repo if true'
+ required: false
+ type: boolean
+ default: false
+ sast_report_name:
+ description: 'Mend SAST scan report name'
+ required: false
+ default: 'Unified_JS_SAST_Scan_Report'
+ type: string
+ sca_report_name:
+ description: 'Mend SCA scan report name'
+ required: false
+ default: 'Unified_JS_SCA_Scan_Report'
+ type: string
+env:
+ MEND_EMAIL: ${{ secrets.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ secrets.MEND_USER_KEY }}
+ MEND_SERVER_URL: ${{ vars.MEND_SERVER_URL }}
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+
+jobs:
+ mend-sast-scan:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ token: ${{ env.GH_TOKEN }}
+
+ - name: Setup Mend
+ uses: ./.github/actions/mend-setup
+
+ - name: Mend SAST scan
+ uses: ./.github/actions/mend-sast
+ timeout-minutes: 10
+ with:
+ scan_dir: ${{ inputs.scan_dir }}
+ full_scan: ${{ inputs.full_scan }}
+ MEND_SAST_REPORT_NAME: ${{ inputs.sast_report_name }}
+ MEND_EMAIL: ${{ env.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ env.MEND_USER_KEY }}
+ MEND_SERVER_URL: ${{ env.MEND_SERVER_URL }}
+
+ mend-sca-scan:
+ runs-on: ubuntu-latest
+ # Force the Mend SCA scan to always run. It is run sequentially after the SAST scan instead of in parallel.
+ needs: mend-sast-scan
+ if: always()
+ steps:
+ - name: Clone the repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ token: ${{ env.GH_TOKEN }}
+
+ - name: Setup Mend
+ uses: ./.github/actions/mend-setup
+
+ - name: Mend SCA scan
+ uses: ./.github/actions/mend-sca
+ timeout-minutes: 10
+ with:
+ scan_dir: ${{ inputs.scan_dir }}
+ full_scan: ${{ inputs.full_scan }}
+ MEND_SCA_REPORT_NAME: ${{ inputs.sca_report_name }}
+ MEND_EMAIL: ${{ env.MEND_EMAIL }}
+ MEND_USER_KEY: ${{ env.MEND_USER_KEY }}
+ MEND_SERVER_URL: ${{ env.MEND_SERVER_URL }}
diff --git a/lefthook.yml b/lefthook.yml
index 88bc8d5168..651e3a4986 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -1,17 +1,23 @@
pre-commit:
+ piped: true
commands:
nx-sync:
+ priority: 1
run: pnpm nx sync
copyright-sync:
+ priority: 2
run: node tools/copyright/sync-header-years.mjs --fix
stage_fixed: true
nx-check:
+ priority: 3
run: pnpm nx affected -t typecheck lint build api-report --tui=false
stage_fixed: true
format:
+ priority: 4
run: pnpm nx format:write
stage_fixed: true
syncpack:
+ priority: 5
glob: >-
{package.json,
pnpm-workspace.yaml,
@@ -23,6 +29,7 @@ pre-commit:
scratchpad/package.json}
run: pnpm syncpack:lint
interface-mapping:
+ priority: 6
glob: >-
{tools/interface-mapping-validator/**/*.ts,
interface_mapping.md,