Repository: vercel/next.js
Pull request: Add custom cleanup script support for deploy tests

## Summary
- Adds `cleanupScriptPath` workflow dispatch input and `NEXT_TEST_CLEANUP_SCRIPT_PATH` env var to `test_e2e_deploy_release.yml` (webpack + turbopack jobs)
- Adds `cleanupUsingCustomScript()` method to `NextDeployInstance` following the same pattern as the existing deploy and logs scripts
- Calls the cleanup script in `destroy()` before existing teardown logic

Changed files:
filename: .github/workflows/test_e2e_deploy_release.yml
patch: @@ -27,6 +27,10 @@ on:
         description: Custom deploy logs script path (NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH)
         default: ''
         type: string
+      cleanupScriptPath:
+        description: Custom cleanup script path (NEXT_TEST_CLEANUP_SCRIPT_PATH)
+        default: ''
+        type: string
 
 env:
   DD_ENV: 'ci'
@@ -122,6 +126,7 @@ jobs:
         VERCEL_CLI_VERSION="${{ github.event.inputs.vercelCliVersion || 'vercel@latest' }}" \
         NEXT_TEST_DEPLOY_SCRIPT_PATH="${{ github.event.inputs.deployScriptPath || '' }}" \
         NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH="${{ github.event.inputs.deployLogsScriptPath || '' }}" \
+        NEXT_TEST_CLEANUP_SCRIPT_PATH="${{ github.event.inputs.cleanupScriptPath || '' }}" \
         node run-tests.js --timings --require-timings -g ${{ matrix.group }} -c 2 --type e2e
       testTimingsArtifact: 'test-timings'
       skipNativeBuild: 'yes'
@@ -166,6 +171,7 @@ jobs:
         VERCEL_CLI_VERSION="${{ github.event.inputs.vercelCliVersion || 'vercel@latest' }}" \
         NEXT_TEST_DEPLOY_SCRIPT_PATH="${{ github.event.inputs.deployScriptPath || '' }}" \
         NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH="${{ github.event.inputs.deployLogsScriptPath || '' }}" \
+        NEXT_TEST_CLEANUP_SCRIPT_PATH="${{ github.event.inputs.cleanupScriptPath || '' }}" \
         node run-tests.js --timings --require-timings -g ${{ matrix.group }} -c 2 --type e2e
       testTimingsArtifact: 'test-timings'
       skipNativeBuild: 'yes'

filename: test/lib/next-modes/next-deploy.ts
patch: @@ -123,6 +123,34 @@ export class NextDeployInstance extends NextInstance {
     return logsRes.stdout + logsRes.stderr
   }
 
+  private async cleanupUsingCustomScript(): Promise<void> {
+    const cleanupScriptPath = process.env.NEXT_TEST_CLEANUP_SCRIPT_PATH!
+
+    require('console').log(
+      `Running cleanup using custom script: ${cleanupScriptPath}`
+    )
+
+    const scriptEnv = {
+      ...process.env,
+      NEXT_TEST_DIR: this.testDir,
+      NEXT_TEST_DEPLOY_URL: this._url,
+      ...this.env,
+    }
+
+    const cleanupRes = await execa(cleanupScriptPath, [], {
+      cwd: this.testDir,
+      env: scriptEnv,
+      reject: false,
+      stderr: 'inherit',
+    })
+
+    if (cleanupRes.exitCode !== 0) {
+      throw new Error(
+        `Custom cleanup script failed: ${cleanupRes.stdout} ${cleanupRes.stderr} (${cleanupRes.exitCode})`
+      )
+    }
+  }
+
   private parseIdsFromCliOuput(): void {
     const buildId = this._cliOutput.match(/BUILD_ID: (.+)/)?.[1]?.trim()
     if (!buildId) {
@@ -438,6 +466,18 @@ export class NextDeployInstance extends NextInstance {
   }
 
   public async destroy() {
+    // Run custom cleanup script if provided
+    const customCleanupScriptPath =
+      process.env.NEXT_TEST_CLEANUP_SCRIPT_PATH?.trim()
+    if (customCleanupScriptPath) {
+      await this.cleanupUsingCustomScript().catch((err) => {
+        require('console').error(
+          'Error running custom cleanup script, continuing with destroy:',
+          err
+        )
+      })
+    }
+
     // If configured, we should remove the proxy address from the hosts file.
     if (this._writtenHostsLine) {
       const trimmed = this._writtenHostsLine.trim()