Testing, CI and DevOps
|
This content was generated with the assistance of AI and should be verified against the official Webflow documentation before being relied on in production. This sub-section’s Webflow bibliography lists the reference material consulted while preparing these pages. |
This page describes how to test and operate the developer side of a Webflow project as it was on 2026-10-28, the date on
which its facts were checked against the CLI, Webflow Cloud, Data API rate-limit and webhook pages of the documentation
on developers.webflow.com and against the public status page. Everything on it can be tried on a Developer workspace
site without a paid plan, with two exceptions: sites on the free tier have the lowest API rate limit, and the number of
backups kept and the availability of branches and Releases depend on the plan. Plans are named by tier only, never by
price; see Plans, Workspaces, Seats and Roles for the tiers. The
pinned tool versions are webflow-api 3.3.4 (Node SDK), webflow 2.0.0 (Python SDK) and @webflow/webflow-cli 2.15.0,
which requires Node.js 22.13.0 or later.
Webflow is a hosted product, so there is no server of your own to patch, and the visual output of the Designer cannot be
unit-tested in the usual sense. What can be tested and automated is everything around it: the published pages, the custom
code and scripts loaded into them, the apps and Cloud deployments, the scripts that write to the CMS and the components
exported to React. The running example is Fernbrook Gardens, a fictitious botanical garden whose developer maintains
fernbrook-sync (a nightly plant import), a Webflow Cloud app called Visit Planner, the PlantCard component exported
with DevLink into a Next.js kiosk, and the BloomCalendar code component.
The delivery model in one picture
The picture contains two ideas that the rest of the page expands. First, Webflow has no staging server that you deploy to
with a command: the Designer is the editing environment and publishing is the deployment, so the staging layer is a
second publish target (the webflow.io domain) plus, on plans that have them, branches and Releases. Second, the
developer artefacts that live outside Webflow (scripts, React apps, Cloud apps) are ordinary code and follow ordinary
CI/CD, with a few Webflow-specific rules about tokens, rate limits and idempotency.
Environments
Developer workspace
A Developer workspace is the free sandbox Webflow offers for building and testing integrations. It is the right place for
everything on this page that writes data: create a throwaway copy of the Fernbrook Gardens site there, point CI at its
site ID and token, and never let a test run use the production site’s token. Keep two sets of secrets in the repository
settings, one per environment, with names that make mixing them up hard (WEBFLOW_API_TOKEN_STAGING and
WEBFLOW_API_TOKEN_PRODUCTION).
The webflow.io staging domain
Every Webflow site can be published to its free webflow.io subdomain independently of its custom domains, and the
publish call in the Data API accepts a list of domain IDs, so a pipeline can publish to staging only. Because the staging
URL serves the same generated HTML, CSS and JavaScript as production, it is the target for browser tests. The URL used
below, https://fernbrook-gardens.webflow.io, is a placeholder for the real subdomain. Domain choices, custom domains and
publish options are described in
Domains, Hosting and Publishing Options and
Publishing and Scheduling.
A rule that matters for automation: the Data API allows one successful site publish per minute, so a pipeline that publishes after every commit must serialise its publishes (see the concurrency group in the sync workflow below).
Branches and Releases
Page branching and Releases let designers work on a page without touching what is live and promote changes after review; they are covered in Page Branching, Releases and Approvals. For a developer the useful consequence is that a branch gives a preview URL where custom code changes can be tested by people, while automated tests still run against the staging domain.
Webflow Cloud environments
Webflow Cloud apps have their own environment model, described in
Webflow Cloud: Deployments and Environments. In summary, each
environment is an isolated container tracking one GitHub branch, and every push to that branch triggers a deployment.
Each environment has a unique mount path under the Webflow domain, so a staging environment of Visit Planner can live
at /planner-staging while production lives at /planner. A failed deployment leaves the last successful deployment
serving, so a broken push does not cause downtime. Environment variables are set per environment (up to 110 per
environment), can be marked secret, and take effect only after a new deployment.
Source: Webflow Cloud: Environments
Testing the published site and custom code
What to test
Webflow itself is not what you test; the things you control are:
-
custom code in the site head and footer and in Code Embeds (see Custom Code in the Head, Footer and Embeds), where a typo breaks every page
-
scripts loaded from a CDN, such as GSAP set-ups (see Interactions with GSAP)
-
forms and their success and error states (see Forms)
-
redirects and localized routes (see Redirects and Site Migration and Localization)
-
a Cloud app mounted under the site, including its routes and its interplay with Webflow pages
-
basic accessibility and SEO invariants of generated pages (title, canonical,
lang, alt text)
The general method of browser testing, including page objects, traces, retries and CI, is covered in the end-to-end testing section: End-to-End Testing, Running E2E Tests in CI/CD and the Playwright pages from Playwright: Getting Started. This page only adds the Webflow-specific parts.
A Playwright project for the staging site
The configuration reads the base URL from an environment variable so the same tests run against staging, a branch preview or the production domain.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
retries: process.env.CI ? 2 : 0,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: process.env.SITE_URL ?? 'https://fernbrook-gardens.webflow.io',
trace: 'on-first-retry',
},
projects: [
{ name: 'desktop', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['Pixel 7'] } },
],
});
Source: Playwright: Test configuration
Two projects, desktop and mobile, matter for Webflow because the published CSS is organised by breakpoints; a rule that works at the desktop breakpoint can overflow at the mobile one (see Responsive Design and Breakpoints).
Smoke tests for custom code and forms
// e2e/smoke.spec.ts
import { test, expect } from '@playwright/test';
test('home page loads without console errors', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); });
page.on('pageerror', (err) => errors.push(err.message));
await page.goto('/');
await expect(page).toHaveTitle(/Fernbrook Gardens/);
await expect(page.locator('html')).toHaveAttribute('lang', /^en/);
expect(errors).toEqual([]);
});
test('bloom reveal runs on scroll', async ({ page }) => {
await page.goto('/plants');
const card = page.locator('[data-bloom-reveal]').first();
await card.scrollIntoViewIfNeeded();
await expect(card).toHaveCSS('opacity', '1');
});
test('group visit form shows the success state', async ({ page }) => {
await page.goto('/visit');
await page.getByLabel('Name').fill('Test Visitor');
await page.getByLabel('Email').fill('test+ci@example.com');
await page.getByRole('button', { name: 'Send enquiry' }).click();
await expect(page.locator('.w-form-done')).toBeVisible();
});
Source: Playwright: Writing tests
The .w-form-done and .w-form-fail classes are the success and error blocks that Webflow generates for every form (see
Forms). A test that submits a real form creates a real submission and, if notifications are
configured, sends an email, so run form tests only against the staging site and point its notification address at a
mailbox for tests.
Redirect and locale checks
Redirects are data, and data deserves a table-driven test. The test below checks a few of the 301 redirects configured for the old site and the Spanish locale route.
// e2e/routes.spec.ts
import { test, expect } from '@playwright/test';
const redirects: Array<[string, string]> = [
['/old-site/orchids', '/plants/orchids'],
['/tickets.html', '/visit'],
];
for (const [from, to] of redirects) {
test(`301 from ${from}`, async ({ request, baseURL }) => {
const res = await request.get(from, { maxRedirects: 0 });
expect(res.status()).toBe(301);
expect(new URL(res.headers()['location'], baseURL).pathname).toBe(to);
});
}
test('Spanish locale is served under /es', async ({ page }) => {
await page.goto('/es');
await expect(page.locator('html')).toHaveAttribute('lang', /^es/);
});
Source: Playwright: APIRequestContext
Testing apps and Cloud routes
A Webflow Cloud app is reached through its mount path, and the Cloud app takes precedence over a Webflow page that has the same path. A route test therefore asserts both the app route and that a Webflow page next to it still renders. For a Designer Extension or a hybrid app (see Designer Extensions and the Designer API and Hybrid Apps), the Designer itself cannot be driven reliably from CI, so test the extension’s logic as plain TypeScript with the Designer API mocked, and test its backend through HTTP.
Contract tests for the CMS sync
The fernbrook-sync script reads a nursery inventory feed and writes Plants items through the Data API. Two contracts
can break without any code change on your side: the feed (the nursery adds, renames or drops a field) and the CMS
schema (an editor deletes or renames a field in the Designer). A contract test checks each boundary without touching
production.
// test/contracts.test.ts
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import { WebflowClient } from 'webflow-api';
const FeedItem = z.object({
sku: z.string(),
commonName: z.string().min(1),
latinName: z.string().min(1),
zone: z.number().int().min(1).max(13),
endangered: z.boolean(),
});
describe('nursery feed', () => {
it('matches the shape the importer expects', async () => {
const res = await fetch(process.env.FEED_URL!);
const items = z.array(FeedItem).parse(await res.json());
expect(items.length).toBeGreaterThan(0);
});
});
describe('Plants collection', () => {
it('still has the fields the importer writes', async () => {
const client = new WebflowClient({ accessToken: process.env.WEBFLOW_API_TOKEN! });
const collection = await client.collections.get(process.env.PLANTS_COLLECTION_ID!);
const slugs = collection.fields?.map((f) => f.slug) ?? [];
for (const required of ['name', 'latin-name', 'hardiness-zone', 'is-endangered']) {
expect(slugs).toContain(required);
}
});
});
Source: Data API: Get Collection Details
Run this suite on a schedule, not only on pushes, because the contracts break on events outside the repository. A failing
feed test tells the team to talk to the nursery; a failing schema test tells them an editor changed a field. For the
CMS-side rules that make schema changes safe, see
CMS Concepts and Content Modeling and the SDK pages
Data API: SDKs, Limits and Errors and
Data API: CMS. The same checks in Python use the webflow 2.0.0 package and pydantic.
# tests/test_contracts.py
import os
from pydantic import BaseModel, Field
from webflow.client import Webflow
class FeedItem(BaseModel):
sku: str
commonName: str = Field(min_length=1)
latinName: str = Field(min_length=1)
zone: int = Field(ge=1, le=13)
endangered: bool
def test_plants_collection_has_required_fields():
client = Webflow(access_token=os.environ["WEBFLOW_API_TOKEN"])
collection = client.collections.get(os.environ["PLANTS_COLLECTION_ID"])
slugs = {field.slug for field in collection.fields}
assert {"name", "latin-name", "hardiness-zone", "is-endangered"} <= slugs
Source: Data API: Get Collection Details
GitHub Actions workflows
All workflows below follow the same security rules, which the CLI documentation also gives for CI: tokens come from
repository or environment secrets and are passed through the WEBFLOW_API_TOKEN environment variable, never as a
--api-token flag that a shell might echo; the CLI is pinned to an exact version because tags such as latest and next
move; and --no-input is passed so a missing value fails fast instead of hanging on a prompt (the CLI also turns prompts
off by itself when CI=true or when there is no terminal).
Source: Webflow CLI: Agents and CI/CD
DevLink export for the kiosk app
The PlantCard component is exported into the kiosk repository. The workflow regenerates the webflow/ folder on demand
and on a schedule, and opens a change for review instead of committing straight to the main branch. Exporting needs a
token with read access to components and the site ID, and the devlink-export block of webflow.json configures the
output (see DevLink Export and Webflow CLI).
name: Export DevLink components
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * 1'
permissions:
contents: write
pull-requests: write
jobs:
export:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm install -g @webflow/webflow-cli@2.15.0
- name: Export components
run: webflow devlink export --site "$WEBFLOW_SITE_ID" --no-input
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN_STAGING }}
WEBFLOW_SITE_ID: ${{ secrets.WEBFLOW_SITE_ID_STAGING }}
- name: Type-check the kiosk against the new components
run: npx tsc --noEmit
- name: Open a pull request when the export changed
run: |
if [ -n "$(git status --porcelain webflow)" ]; then
git config user.name "fernbrook-ci"
git config user.email "ci@example.com"
git checkout -b "devlink/export-${GITHUB_RUN_ID}"
git add webflow
git commit -m "Update exported DevLink components"
git push origin HEAD
gh pr create --fill
fi
env:
GH_TOKEN: ${{ github.token }}
Source: Webflow CLI: Configuration
The type-check step is the cheap test that catches the usual breakage: a designer renamed a component property in Webflow, the export changed the React prop name and the kiosk no longer compiles. Reviewing the export as a pull request makes that change visible to the developers before it reaches the kiosk’s main branch.
Code component import
Importing bundles the React components of the BloomCalendar library and uploads them to the workspace as a shared
library. Two documented points shape the workflow: --no-input skips the prompts for CI/CD, and a pipeline should detect
changes so that components are not removed by accident; the bundle is limited to 50 MB, which includes dependencies and
assets such as images and fonts, and both import and bundle exit with 0 on success, so any non-zero code is a
failure. See Code Components.
name: Import code components
on:
push:
branches: [main]
paths:
- 'components/**'
- 'webflow.json'
jobs:
import:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm install -g @webflow/webflow-cli@2.15.0
- name: Check the bundle size locally
run: |
webflow devlink bundle --public-path http://localhost:4000/
du -sm dist
- name: Share the library with the workspace
run: webflow devlink import --no-input
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_WORKSPACE_API_TOKEN }}
The paths filter is the simple form of change detection: the import runs only when component sources or the manifest
changed. The secret holds a Workspace API token, which the CLI reads from WEBFLOW_API_TOKEN in non-interactive runs (see
Webflow CLI for the authentication modes).
Webflow Cloud deploys
The recommended route for continuous deployment of a Cloud app is the GitHub connection in the dashboard: every push to the
branch an environment tracks produces a deployment, and a failed deployment keeps the previous one live. A workflow is
needed only for custom build steps, per-environment secrets or deploy gates. The CLI command is webflow cloud deploy on
the stable channel; the apps family, which gives deployment history, waiting and idempotency, exists on the pre-release
channel (see Webflow CLI for the channels).
name: Deploy Visit Planner
on:
push:
branches: [main]
concurrency:
group: deploy-visit-planner
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci && npm test
- name: Install Webflow CLI
run: npm install -g @webflow/webflow-cli@next # pin an exact pre-release version in a real pipeline
- name: Deploy
run: |
webflow apps deploy --no-input \
--site-id "$WEBFLOW_SITE_ID" \
--mount /planner \
--environment production \
--skip-mount-path-check
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
WEBFLOW_SITE_ID: ${{ secrets.WEBFLOW_SITE_ID }}
- name: Wait for the deployment to finish
run: |
DEP_ID=$(webflow apps deployments list --limit 1 --json | jq -r '.deployments[0].id')
webflow apps deployments get "$DEP_ID" --wait --timeout 300
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
Source: Webflow CLI: Agents and CI/CD
Four details make this workflow safe. --mount and --environment are passed together because without both the deploy
still prompts and would hang. The concurrency group serialises deploys so two pushes cannot race. The wait step turns
the asynchronous deployment into a pass or fail result (exit code 0 means success; 1 covers any other outcome and a
timeout, so read the reported status to tell them apart). Finally, retries should not queue duplicate builds:
webflow apps deployments trigger --idempotency-key "$GITHUB_RUN_ID"
webflow apps deployments redeploy REPLACE_WITH_DEPLOYMENT_ID --idempotency-key "$GITHUB_RUN_ID"
Source: Webflow CLI: Agents and CI/CD
Both commands require a GitHub-connected app. Secrets for the app itself, such as the key that Visit Planner uses for a map service, are set per environment, and the documentation shows piping the value through standard input so it does not land in shell history or the process list:
printf '%s' "$MAP_KEY" | webflow apps env-vars set MAP_KEY --secret \
--app-id REPLACE_WITH_APP_ID --environment-id REPLACE_WITH_ENVIRONMENT_ID
Source: Webflow CLI: Agents and CI/CD
Environment-variable changes take effect only after the next deployment, and the variable page says secret values are masked in the dashboard and redacted from build logs. Do not print them anyway.
Rollbacks. Webflow Cloud has no one-click restore of a past build in the way a static host does. The documented rollback is
to revert the tracked branch to the wanted commit and push, which triggers a new deployment of the earlier code; the
documentation notes that this does not restore the exact state of the previous deployment, so anything the app stored in
its databases is untouched. With the apps family, redeploy reruns a deployment at the same commit.
Source: Webflow Cloud: Deployments
Logs for diagnosing a failed deployment are in the environment’s Deployment Dashboard: build logs per deployment ID (the commit SHA) and a Runtime Logs tab with server-side output and errors. See Webflow Cloud: Deployments and Environments for the full model.
CMS sync with secrets
The nightly import runs fernbrook-sync. Its design rules are those of any job that writes to a shared system: a dry-run
mode, a concurrency group so two runs never overlap, a backup before writing, and idempotency so a retry cannot create
duplicates (the script matches nursery SKUs to existing items by a SKU field and updates rather than creates).
name: Nightly plant sync
on:
schedule:
- cron: '30 2 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Report changes without writing'
type: boolean
default: true
concurrency:
group: fernbrook-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- name: Contract tests
run: npm run test:contracts
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
PLANTS_COLLECTION_ID: ${{ vars.PLANTS_COLLECTION_ID }}
FEED_URL: ${{ vars.FEED_URL }}
- name: Export a backup of the Plants items
run: node scripts/backup-plants.mjs "backup/plants-${GITHUB_RUN_ID}.json"
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
PLANTS_COLLECTION_ID: ${{ vars.PLANTS_COLLECTION_ID }}
- uses: actions/upload-artifact@v4
with:
name: plants-backup
path: backup/
retention-days: 30
- name: Sync
run: node scripts/sync.mjs ${{ (github.event_name == 'workflow_dispatch' && inputs.dry_run) && '--dry-run' || '' }}
env:
WEBFLOW_API_TOKEN: ${{ secrets.WEBFLOW_API_TOKEN }}
PLANTS_COLLECTION_ID: ${{ vars.PLANTS_COLLECTION_ID }}
FEED_URL: ${{ vars.FEED_URL }}
Source: Data API: Rate limits
The token is a site token (or a workspace token for several sites) with only the scopes the job needs: CMS read and write
for the Plants collection and nothing else. Scopes and token types are explained in
Data API: Authentication and Scopes. Using a GitHub
environment named production lets the repository require a manual approval before the job can read the production
secrets.
Rate limits, retries and idempotency
The Data API limits requests per minute per API key by plan: 60 for the Starter and Basic site plans, 120 for CMS,
Ecommerce and Business, and a custom limit for Enterprise. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining;
exceeding the limit returns HTTP 429 with the error code too_many_requests and a Retry-After header, typically 60
seconds. The official SDK retries with exponential backoff by itself; a hand-written client must wait for the
Retry-After value. A site publish is limited to one successful publish per minute, and the documentation recommends
webhooks over polling to stay under the limit.
Source: Data API: Rate limits
A sync that writes hundreds of plants should therefore batch (the bulk CMS endpoints accept many items per call), throttle
itself below the limit and treat a 429 as a signal to pause. A wrapper such as the following makes the pause explicit when
a raw fetch is used instead of the SDK.
async function withRetry<T>(call: () => Promise<Response>, parse: (r: Response) => Promise<T>): Promise<T> {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await call();
if (res.status !== 429) {
if (!res.ok) throw new Error(`Webflow API error ${res.status}`);
return parse(res);
}
const waitSeconds = Number(res.headers.get('retry-after') ?? '60');
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
}
throw new Error('Rate limit retries exhausted');
}
Source: Data API: Rate limits
Idempotency is the second half of safe retries. A retried create writes a second item; a retried update does not. Prefer matching on a stable key (the SKU stored in a field) and updating, and, for deployments, pass the CI run ID as the idempotency key as shown above. Details of the SDK retry options and the error catalogue are in Data API: SDKs, Limits and Errors.
Monitoring
The status page
The public status page at https://status.webflow.com lists components grouped by area, including Hosted Websites, Site Pages, Ecommerce, Forms, Webflow Canvas, Dashboard, Publishing, the API and MCP server, Analyze and Email Notifications. It offers subscriptions by email, SMS and Slack, an Atom and an RSS feed of incident history, and webhook notifications that call a URL you provide on incident creation, update and resolution and on component status changes. Subscribe the team’s incident channel with a webhook so that a failed nightly sync can be compared with a Webflow incident before anyone debugs the script.
Source: Webflow status
Webhooks from the site
Site webhooks turn publishes and submissions into events your monitoring can use. The event types include
form_submission, site_publish, collection_item_created, collection_item_changed, collection_item_published and
collection_item_unpublished among others. Webflow expects a 200 response; any other result, including redirects, TLS
errors and timeouts, counts as a failure and is retried up to three more times, ten minutes apart. Repeated failures
deactivate the webhook and send an email, so a receiver that goes down silently loses events: alert on the receiver’s own
health. There is a limit of 75 webhooks per trigger type. Receivers must verify the x-webflow-signature header, an HMAC
SHA-256 of the timestamp, a colon and the raw body, and should reject requests older than five minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebflow(rawBody: string, timestamp: string, signature: string, secret: string): boolean {
const sent = Number(timestamp);
const sentMs = sent > 1e12 ? sent : sent * 1000; // Unix epoch time: accept seconds or milliseconds
const ageMs = Date.now() - sentMs;
if (!timestamp || !Number.isFinite(ageMs) || Math.abs(ageMs) > 5 * 60 * 1000) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}:${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
Source: Data API: Working with webhooks
The documentation’s example timestamp (1722370035277) is in milliseconds, which is why the function compares against
Date.now() directly. The full receiver for Fernbrook Gardens, with form submissions and item publishes, is on
Webhooks.
Synthetic checks and metrics
Add a scheduled run of the smoke tests against the production domain (every hour is enough for most sites) and alert on
failure; it is the only check that covers DNS, the CDN, custom code and forms together. Page speed and Core Web Vitals
belong to Performance Optimization and
Analyze and Optimize. Also record the deployment history of Cloud apps and
the X-RateLimit-Remaining header of a sync run, so that a growing sync that approaches its limit is noticed before it
fails.
Backups before bulk API writes
Webflow’s own backups (see Backups and Recovery) are Designer-level restore points; they are not an API and restoring one rolls the whole site back, which is a poor tool for undoing a bad import of two hundred plants. Before any bulk write, take a data-level backup that can restore individual items:
-
export the collection to CSV from the CMS panel (see CSV Import and Export), or list items through the API and store the JSON, as
scripts/backup-plants.mjsdoes in the workflow above -
keep the backup as a CI artifact with a retention period, and keep the IDs, because restoring means updating items by ID
-
create a manual Webflow backup of the site before an unusually large change
-
run the first execution of any new script with
--dry-run, and on a Developer workspace copy of the site, before it touches production -
for a script that deletes, require an explicit flag and print the number of items affected before acting
A backup is only useful if restore has been rehearsed, so include a restore script that reads the JSON and updates items by ID, and run it once against the test site.
Common pitfalls
-
Production token in CI for tests. A leaked or misused token writes to the live site. Use separate tokens, scopes and GitHub environments per target.
-
Tests that depend on editor content. Staging content changes; assert on structure and attributes you control, not on copy an editor may rewrite.
-
Unpinned CLI.
latestandnextmove, and so do command names between channels. Pin an exact version. -
Prompts in CI. A missing flag hangs a job unless
--no-inputis set;--mountand--environmentmust be given together forapps deploy. -
Publishing on every push. One successful publish per minute is allowed; serialise publishes with a concurrency group.
-
Webhook receiver without a health check. Repeated failures deactivate the webhook; monitor the receiver.
-
Secrets in logs. Pass secrets by environment variable or standard input, never as arguments or echoed values.
-
Assuming a rollback restores data. Redeploying earlier code does not restore database content written by the app.