Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-870.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sanity/cli': minor
---

add E2E testing package and CI workflow
127 changes: 127 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: E2E Tests

on:
pull_request:
push:
branches:
- main
workflow_dispatch:
inputs:
cli_version:
description: 'npm version/tag to test (e.g., latest, 3.82.0)'
required: true
default: 'latest'
cli_package:
description: 'Package to install — must provide the "sanity" binary (e.g., @sanity/cli, sanity)'
required: false
default: '@sanity/cli'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
should_run: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.filter.outputs.cli == 'true' }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
id: filter
with:
filters: |
cli:
- 'packages/@sanity/cli/**'
- 'packages/@sanity/cli-core/**'
- 'packages/@sanity/cli-e2e/**'
- 'packages/@sanity/cli-test/**'
- 'packages/create-sanity/**'
- 'fixtures/**'
- 'pnpm-lock.yaml'

e2e:
needs: changes
if: needs.changes.outputs.should_run == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
strategy:
matrix:
os: [ubuntu-latest]
node-version: [20, 22, 24]
fail-fast: false

steps:
- uses: actions/checkout@v6

- name: Setup Environment
uses: ./.github/actions/setup
with:
node-version: ${{ matrix.node-version }}

- name: Build CLI (pack mode)
if: ${{ !inputs.cli_version }}
run: pnpm build:cli

- name: Install CLI from npm (registry mode)
if: ${{ inputs.cli_version }}
run: |
INSTALL_DIR=$(mktemp -d)
PACKAGE="${{ inputs.cli_package || '@sanity/cli' }}"
npm install --prefix "$INSTALL_DIR" "${PACKAGE}@${{ inputs.cli_version }}"
echo "E2E_BINARY_PATH=$INSTALL_DIR/node_modules/.bin/sanity" >> $GITHUB_ENV
"$INSTALL_DIR/node_modules/.bin/sanity" --version

- name: Run E2E tests
env:
SANITY_E2E_TOKEN: ${{ secrets.SANITY_E2E_TOKEN }}
SANITY_E2E_PROJECT_ID: ${{ secrets.SANITY_E2E_PROJECT_ID }}
SANITY_E2E_DATASET: ${{ secrets.SANITY_E2E_DATASET }}
run: pnpm --filter @sanity/cli-e2e test

notify-failure:
if: ${{ failure() && inputs.cli_version }}
needs: [e2e]
runs-on: ubuntu-latest
steps:
- name: Notify Slack on failure
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
webhook: ${{ secrets.SLACK_E2E_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "Post-release E2E failed for ${{ inputs.cli_package || '@sanity/cli' }}@${{ inputs.cli_version }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":red_circle: *Post-release E2E failed*\n*Package:* `${{ inputs.cli_package || '@sanity/cli' }}@${{ inputs.cli_version }}`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
}
}
]
}

e2e-status:
if: always()
needs: [changes, e2e]
runs-on: ubuntu-latest
steps:
- name: Check status
run: |
# Fail if any needed job failed or was cancelled
results=("${{ needs.changes.result }}" "${{ needs.e2e.result }}")
for result in "${results[@]}"; do
if [ "$result" == "failure" ] || [ "$result" == "cancelled" ]; then
echo "Job failed or was cancelled: $result"
exit 1
fi
done
echo "All jobs passed or were skipped"
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ concurrency:
cancel-in-progress: false

permissions:
actions: write # for gh workflow run in post-release
contents: write # for version bump commits and tags
pull-requests: write # for creating Version Packages PR
id-token: write # to enable use of OIDC for npm provenance
Expand All @@ -20,6 +21,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
published: ${{ steps.changesets.outputs.published }}
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}

env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
Expand Down Expand Up @@ -104,3 +106,16 @@ jobs:
repository_name: ${{ github.event.repository.name }}
initial_state_id: 'c56956cd-c281-4ca5-889f-6189ce231a6d'
done_state_id: '5a35b7bf-6d37-4cc2-854a-2f18d160e2e5'

- name: Trigger post-release E2E tests
env:
GH_TOKEN: ${{ github.token }}
PUBLISHED_PACKAGES: ${{ needs.release.outputs.publishedPackages }}
run: |
CLI_VERSION=$(echo "$PUBLISHED_PACKAGES" | jq -r '.[] | select(.name == "@sanity/cli") | .version')
if [ -n "$CLI_VERSION" ]; then
echo "Triggering E2E tests for @sanity/cli@$CLI_VERSION"
gh workflow run e2e.yml -f cli_version="$CLI_VERSION"
else
echo "No @sanity/cli in published packages, skipping E2E"
fi
4 changes: 2 additions & 2 deletions fixtures/basic-studio/sanity.cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
api: {
dataset: 'test',
projectId: 'ppsg7ml5',
dataset: process.env.SANITY_E2E_DATASET || 'test',
projectId: process.env.SANITY_E2E_PROJECT_ID || 'ppsg7ml5',
},
deployment: {
autoUpdates: true,
Expand Down
9 changes: 9 additions & 0 deletions fixtures/nextjs-app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const metadata = {title: 'Test App'}

export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
3 changes: 3 additions & 0 deletions fixtures/nextjs-app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return <main>Hello</main>
}
2 changes: 2 additions & 0 deletions fixtures/nextjs-app/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const nextConfig = {}
export default nextConfig
18 changes: 18 additions & 0 deletions fixtures/nextjs-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "nextjs-app-fixture",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build"
},
"dependencies": {
"next": "^16.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"typescript": "^5.8.3"
}
}
21 changes: 21 additions & 0 deletions fixtures/nextjs-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{"name": "next"}],
"paths": {"@/*": ["./*"]}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
6 changes: 6 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ const baseConfig = {
],
project,
},
'packages/@sanity/cli-e2e': {
entry: [],
// @sanity/cli and create-sanity are resolved dynamically via require.resolve() in packCli.ts
ignoreDependencies: ['@sanity/cli', 'create-sanity'],
project: ['helpers/**/*.{js,ts}', '__tests__/**/*.{js,ts}'],
},
'packages/@sanity/cli-test': {
entry: ['package.config.ts'],
project,
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"check:deps": "knip",
"check:format": "oxfmt --check",
"check:lint": "turbo run lint -- --fix",
"check:types": "turbo run check:types --filter=@sanity/cli --filter=@sanity/cli-core",
"check:types": "turbo run check:types --filter=@sanity/cli --filter=@sanity/cli-core --filter=@sanity/cli-e2e",
"clean": "rimraf packages/@sanity/*/lib packages/*/lib packages/@sanity/*/dist packages/*/dist",
"clean:deps": "rimraf packages/*/node_modules fixtures/*/node_modules node_modules",
"depcheck": "knip",
Expand All @@ -41,6 +41,8 @@
"release": "pnpm build:cli && pnpm publish-packages",
"pretest": "pnpm run build:cli",
"test": "vitest run",
"pretest:e2e": "pnpm run build:cli",
"test:e2e": "pnpm --filter @sanity/cli-e2e test",
"pretest:watch": "pnpm run build:cli",
"test:watch": "vitest",
"version-packages": "changeset version",
Expand Down Expand Up @@ -75,7 +77,8 @@
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core",
"esbuild"
"esbuild",
"node-pty"
],
"overrides": {
"@sanity/cli": "workspace:*"
Expand Down
2 changes: 2 additions & 0 deletions packages/@sanity/cli-e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SANITY_E2E_TOKEN=
SANITY_E2E_PROJECT_ID=
5 changes: 5 additions & 0 deletions packages/@sanity/cli-e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
coverage
tmp
dist
.test-output
17 changes: 17 additions & 0 deletions packages/@sanity/cli-e2e/__tests__/createSanity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {describe, expect, test} from 'vitest'

import {runCli} from '../helpers/runCli.js'

const createSanityBinary = process.env.E2E_CREATE_SANITY_BINARY_PATH

describe.skipIf(!createSanityBinary)('create-sanity', () => {
test('--help prints usage and exits 0', async () => {
const {error, stdout} = await runCli({
args: ['--help'],
binaryPath: createSanityBinary!,
})

if (error) throw error
expect(stdout).toContain('Initialize a new Sanity Studio')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {execFileSync} from 'node:child_process'

import {describe, expect, test} from 'vitest'

import {getAvailablePackageManagers} from '../helpers/packageManagers.js'

// Only run against a known-published version (set by post-release CI).
// The local workspace version may not exist on npm, and package managers
// fetch from the registry, so falling back to it would cause false failures.
const version = process.env.E2E_PACKAGE_VERSION

describe.skipIf(!version)('create-sanity via package managers', () => {
const managers = getAvailablePackageManagers()

for (const pm of managers) {
describe(pm.name, () => {
test(`${pm.name} create sanity@${version} --help exits 0`, () => {
const [cmd, ...args] = pm.createCommand(version!, ['--help'])

let result: string
try {
result = execFileSync(cmd, args, {
encoding: 'utf8',
env: {
...process.env,
NO_UPDATE_NOTIFIER: '1',
NODE_ENV: 'production',
NODE_NO_WARNINGS: '1',
},
stdio: 'pipe',
timeout: 60_000,
})
} catch (err) {
const stderr = (err as {stderr?: Buffer | string}).stderr
throw new Error(`${cmd} failed:\n${String(stderr || err)}`, {cause: err})
}

expect(result).toContain('Initialize a new Sanity Studio')
})
})
}
})
Loading
Loading