PWA Today documentation

Integrating PWA Today Audits into CI/CD

Run runtime audits after deployment or coordinate a deployment to verify service worker updates.

This guide explains how to run PWA Today runtime audits from a CI/CD pipeline. The basic integration works with any CI provider that can run Node.js and curl.

There are two integration modes:

  1. Run an audit after an existing deployment.
  2. Let the audit coordinate the deployment so it can test a service worker update.

Use the first mode for general runtime checks. Use the second mode when the audit must verify the transition from the currently deployed service worker to the new version.

Related documentation:

Prerequisites

You need:

  • Node.js 18 or newer.
  • A PWA Today customer account.
  • A PWA Today client ID and client secret.
  • The public URL of the application to audit.

Retrieve the client ID and client secret from the credentials page in the PWA Today customer dashboard.

Install the CLI

Install the CLI as a development dependency:

npm install --save-dev @pwa-today/pwa-check

Commit package.json and package-lock.json so CI installs the same version:

git add package.json package-lock.json
git commit -m "Add PWA Today audits"

The examples below use the locally installed executable:

npx pwa-check

Configure CI secrets

Add these protected or secured variables to the CI/CD platform:

Variable Description
PWA_CLIENT_ID Customer API client ID
PWA_CLIENT_SECRET Customer API client secret
PWA_AUDIT_URL Public URL to audit, such as https://example.com

Do not commit the client secret or an access token to the repository.

The pipeline should exchange the client credentials for a short-lived access token each time it runs:

export PWA_AUDIT_TOKEN="$(
  curl --silent --show-error --fail \
    --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
    --request POST \
    https://api.pwa.today/token |
  node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
)"

PWA_AUDIT_TOKEN must be available to the pwa-check audit process. Do not print it or store it as a build artifact.

Add an audit configuration

Create pwa-check.yml in the repository root:

version: 1

audit:
  # quick, standard, full, or custom
  profile: standard

  # Optional. Defaults to the audited hostname.
  applicationId: example.com

  include: []
  exclude: []

qualityGate:
  minimumScore: 90
  failOn:
    - critical
    - high

reports:
  json: reports/pwa-audit.json
  junit: reports/pwa-audit.xml

The CLI automatically loads pwa-check.yml, pwa-check.yaml, or pwa-check.json from the current directory.

The audit configuration selects the checks to run. The quality gate determines whether a completed audit should fail the pipeline.

Run an audit after deployment

This is the simplest integration and works with an existing automatic or pipeline-managed deployment:

set -euo pipefail

./scripts/deploy-and-wait.sh

export PWA_AUDIT_TOKEN="$(
  curl --silent --show-error --fail \
    --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
    --request POST \
    https://api.pwa.today/token |
  node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
)"

npx pwa-check audit "$PWA_AUDIT_URL"

The deployment step must finish before the audit starts. If the hosting provider deploys automatically after a push, configure the audit as a later pipeline step or job that waits for that deployment.

This mode checks the deployed application, but it cannot observe the service worker transition because the deployment has already happened.

Test a service worker deployment

The service-worker-deployment check must capture the old service worker before deployment. The CLI therefore needs to control the deployment:

  1. The hosted check captures the current application and service worker.
  2. The CLI runs the configured deployment command.
  3. The deployment command waits until the new version is publicly available.
  4. The CLI signals that deployment has completed.
  5. The hosted check verifies the service worker update.
  6. The CLI starts the remaining runtime checks.

Enable it explicitly in pwa-check.yml:

version: 1

audit:
  profile: full
  applicationId: example.com

  include:
    - service-worker-deployment

  options:
    service-worker-deployment:
      deploymentTimeout: 900000
      commandTimeout: 900000
      command:
        - ./scripts/deploy-and-wait.sh

qualityGate:
  minimumScore: 90
  failOn:
    - critical
    - high

reports:
  json: reports/pwa-audit.json
  junit: reports/pwa-audit.xml

Run the same audit command:

npx pwa-check audit "$PWA_AUDIT_URL"

Do not run the deployment command separately in this mode. pwa-check runs it after the pre-deployment baseline is ready.

For complete Vercel, Netlify, and AWS Amplify Hosting examples, see Service Worker Deployment Testing.

Deployment script contract

./scripts/deploy-and-wait.sh is application and hosting-provider specific. It must:

  • Start deployment of the commit being audited.
  • Wait until that deployment has finished and is publicly observable.
  • Exit with code 0 after a successful deployment.
  • Exit with a non-zero code if deployment fails.

Make the script executable:

chmod +x scripts/deploy-and-wait.sh

The command is executed directly without a shell. Put pipelines, redirects, multiple commands, and environment setup in the script rather than in the command array.

The deployment must contain a changed service worker. For example, increment a service worker version or otherwise change its generated output. A deployment that leaves the service worker unchanged cannot verify an update.

If the deployment command fails or times out, the CLI cancels the hosted deployment check and releases its URL lock.

Automatic hosting deployments

An automatic deployment triggered immediately by a Git push can finish before the audit captures its baseline. That makes a service worker deployment test unreliable.

For this mode, configure the CI pipeline as the deployment orchestrator:

  1. Prevent the hosting provider from deploying the branch before the audit starts.
  2. Let pwa-check invoke deploy-and-wait.sh.
  3. Have that script start and monitor the hosting-provider deployment.

The exact implementation of the script depends on the hosting provider. For example, it might use the AWS Amplify CLI/API, the Vercel CLI, the Netlify CLI, or an internal deployment API.

GitHub Actions example

name: Deploy and audit

on:
  push:
    branches:
      - main

jobs:
  production:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Run PWA audit
        env:
          PWA_CLIENT_ID: ${{ secrets.PWA_CLIENT_ID }}
          PWA_CLIENT_SECRET: ${{ secrets.PWA_CLIENT_SECRET }}
          PWA_AUDIT_URL: ${{ vars.PWA_AUDIT_URL }}
        run: |
          export PWA_AUDIT_TOKEN="$(
            curl --silent --show-error --fail \
              --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
              --request POST \
              https://api.pwa.today/token |
            node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
          )"

          npx pwa-check audit "$PWA_AUDIT_URL"

      - name: Upload audit reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: pwa-audit
          path: reports/
          if-no-files-found: ignore

GitLab CI example

pwa-audit:
  image: node:22
  stage: deploy

  script:
    - npm ci
    - |
      export PWA_AUDIT_TOKEN="$(
        curl --silent --show-error --fail \
          --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
          --request POST \
          https://api.pwa.today/token |
        node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
      )"

      npx pwa-check audit "$PWA_AUDIT_URL"

  artifacts:
    when: always
    paths:
      - reports/
    reports:
      junit: reports/pwa-audit.xml

Add the three variables under the project’s CI/CD variables and protect or mask the client secret.

Bitbucket Pipelines example

image: node:22

pipelines:
  branches:
    main:
      - step:
          name: Deploy and audit production
          deployment: production
          max-time: 30
          caches:
            - node

          script:
            - npm ci
            - |
              export PWA_AUDIT_TOKEN="$(
                curl --silent --show-error --fail \
                  --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
                  --request POST \
                  https://api.pwa.today/token |
                node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
              )"

              npx pwa-check audit "$PWA_AUDIT_URL"

          artifacts:
            - reports/**

If the default branch has another name, replace main.

Store PWA_CLIENT_ID, PWA_CLIENT_SECRET, and PWA_AUDIT_URL as repository or deployment variables. Mark the client secret as secured.

Azure DevOps example

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: 22.x

  - script: npm ci
    displayName: Install dependencies

  - bash: |
      export PWA_AUDIT_TOKEN="$(
        curl --silent --show-error --fail \
          --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
          --request POST \
          https://api.pwa.today/token |
        node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
      )"

      npx pwa-check audit "$PWA_AUDIT_URL"
    displayName: Run PWA audit
    env:
      PWA_CLIENT_ID: $(PWA_CLIENT_ID)
      PWA_CLIENT_SECRET: $(PWA_CLIENT_SECRET)
      PWA_AUDIT_URL: $(PWA_AUDIT_URL)

  - task: PublishPipelineArtifact@1
    condition: always()
    inputs:
      targetPath: reports
      artifact: pwa-audit

Add the three variables under the pipeline variables. Mark PWA_CLIENT_SECRET as secret. Azure DevOps secret variables must be mapped under env before a script can access them.

CircleCI example

version: 2.1

jobs:
  pwa-audit:
    docker:
      - image: cimg/node:22.14

    steps:
      - checkout

      - run:
          name: Install dependencies
          command: npm ci

      - run:
          name: Run PWA audit
          command: |
            export PWA_AUDIT_TOKEN="$(
              curl --silent --show-error --fail \
                --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
                --request POST \
                https://api.pwa.today/token |
              node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
            )"

            npx pwa-check audit "$PWA_AUDIT_URL"

      - store_artifacts:
          path: reports
          destination: pwa-audit

workflows:
  production-audit:
    jobs:
      - pwa-audit:
          filters:
            branches:
              only: main

Add PWA_CLIENT_ID, PWA_CLIENT_SECRET, and PWA_AUDIT_URL under the project’s environment variables or in a restricted context.

Jenkins example

pipeline {
  agent {
    docker {
      image 'node:22'
    }
  }

  environment {
    PWA_AUDIT_URL = 'https://example.com'
  }

  stages {
    stage('Install dependencies') {
      steps {
        sh 'npm ci'
      }
    }

    stage('Run PWA audit') {
      steps {
        withCredentials([
          string(credentialsId: 'pwa-client-id', variable: 'PWA_CLIENT_ID'),
          string(credentialsId: 'pwa-client-secret', variable: 'PWA_CLIENT_SECRET')
        ]) {
          sh '''
            export PWA_AUDIT_TOKEN="$(
              curl --silent --show-error --fail \
                --user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
                --request POST \
                https://api.pwa.today/token |
              node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
            )"

            npx pwa-check audit "$PWA_AUDIT_URL"
          '''
        }
      }
    }
  }

  post {
    always {
      archiveArtifacts artifacts: 'reports/**', allowEmptyArchive: true
      junit testResults: 'reports/pwa-audit.xml', allowEmptyResults: true
    }
  }
}

Add the client ID and client secret to Jenkins as Secret text credentials with the IDs used in the Jenkinsfile. Replace PWA_AUDIT_URL with the deployed application URL.

Check-specific configuration

Some checks need application-specific input. Add it under audit.options:

audit:
  profile: full

  options:
    offline-navigation:
      series:
        - [/, /products, /checkout]
        - [/, /account]

    offline-request-retry:
      requestUrl: https://example.com/api/messages
      method: POST
      requestBody:
        message: Audit test request

    push-notifications:
      payload:
        title: PWA audit
        message: Test notification

Checks without required configuration return not-applicable. Use dedicated test data and endpoints for checks that create requests or notifications.

Exit codes

The CLI returns:

Exit code Meaning
0 Audit and quality gate passed
1 Quality gate failed
2 Authentication or configuration error
3 Audit service error or timeout

CI/CD systems normally fail the job automatically when the command returns a non-zero exit code.

Reports

Configure JSON and JUnit report paths in pwa-check.yml:

reports:
  json: reports/pwa-audit.json
  junit: reports/pwa-audit.xml

The reports are created relative to the pipeline’s working directory. Configure the CI/CD provider to retain reports/ as a build artifact. Reports are not committed to the repository.

Audit history and results are also available in the PWA Today customer dashboard.

Troubleshooting

Authentication fails

Confirm that:

  • Both client credentials are available in the same pipeline step as the token request.
  • The client secret is not surrounded by unintended quotes or whitespace.
  • The token request uses HTTP Basic authentication.
  • PWA_AUDIT_TOKEN is exported in the process that runs the CLI.

The deployment command fails

Run deploy-and-wait.sh directly in the same CI image. Confirm that the pipeline has the hosting-provider credentials it needs and that the script does not exit before deployment finishes.

A deployment check is already active

Only one service worker deployment check can run for a URL at a time. Avoid concurrent production deployments for the same URL. A failed deployment command is cancelled automatically, but an interrupted CI process may leave a check active until its timeout expires.

The service worker did not update

Confirm that:

  • The deployed service worker file changed.
  • The deployment finished before deploy-and-wait.sh exited.
  • The service worker is served without stale intermediary caching.
  • The audited URL is within the service worker’s scope.