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:
- Run an audit after an existing deployment.
- 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:
- The hosted check captures the current application and service worker.
- The CLI runs the configured deployment command.
- The deployment command waits until the new version is publicly available.
- The CLI signals that deployment has completed.
- The hosted check verifies the service worker update.
- 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
0after 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:
- Prevent the hosting provider from deploying the branch before the audit starts.
- Let
pwa-checkinvokedeploy-and-wait.sh. - 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_TOKENis 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.shexited. - The service worker is served without stale intermediary caching.
- The audited URL is within the service worker’s scope.
Run an audit