Repository: vercel/next.js
Pull request: feat(test): add proxy support for e2e deployment tests

### What?

Adds proxy configuration support for e2e deployment tests, allowing test traffic to be routed through a specified proxy server.

### Why?

Some testing environments require network traffic to be routed through specific proxy servers or load balancers. This change enables the e2e deployment test infrastructure to support such configurations by:

- Allowing specification of a proxy IP address via the `NEXT_TEST_PROXY_ADDRESS` environment variable
- Automatically managing `/etc/hosts` entries to route deployment domains through the proxy
- Providing proper cleanup when tests complete

Changed files:
filename: .github/workflows/build_reusable.yml
patch: @@ -74,6 +74,11 @@ on:
         required: false
         type: string
         default: 'x86_64-unknown-linux-gnu'
+      overrideProxyAddress:
+        description: Override the proxy address to use for the test
+        required: false
+        type: string
+        default: ''
 
 env:
   NAPI_CLI_VERSION: 2.18.4
@@ -101,6 +106,7 @@ env:
   VERCEL_TEST_TEAM: vtest314-next-e2e-tests
   NEXT_TEST_PREFER_OFFLINE: 1
   NEXT_CI_RUNNER: ${{ inputs.runs_on_labels }}
+  NEXT_TEST_PROXY_ADDRESS: ${{ inputs.overrideProxyAddress || '' }}
 
 jobs:
   build:

filename: .github/workflows/test_e2e_deploy_release.yml
patch: @@ -15,6 +15,10 @@ on:
         description: Version of Vercel CLI to use
         default: 'vercel@latest'
         type: string
+      overrideProxyAddress:
+        description: Override the proxy address to use for the test
+        default: ''
+        type: string
 
 env:
   VERCEL_TEST_TEAM: vtest314-next-e2e-tests
@@ -80,6 +84,7 @@ jobs:
       stepName: 'test-deploy-${{ matrix.group }}'
       timeout_minutes: 180
       runs_on_labels: '["ubuntu-latest"]'
+      overrideProxyAddress: ${{ inputs.overrideProxyAddress || '' }}
 
   report-test-results-to-datadog:
     needs: test-deploy

filename: test/lib/next-modes/next-deploy.ts
patch: @@ -14,6 +14,7 @@ import { Span } from 'next/dist/trace'
 export class NextDeployInstance extends NextInstance {
   private _cliOutput: string
   private _buildId: string
+  private _writtenHostsLine: string | null = null
 
   public get buildId() {
     // get deployment ID via fetch since we can't access
@@ -127,10 +128,41 @@ export class NextDeployInstance extends NextInstance {
         `Failed to deploy project ${deployRes.stdout} ${deployRes.stderr} (${deployRes.exitCode})`
       )
     }
+
     // the CLI gives just the deployment URL back when not a TTY
     this._url = deployRes.stdout
     this._parsedUrl = new URL(this._url)
 
+    // If configured, we should configure the `/etc/hosts` file to point the
+    // deployment domain to the specified proxy address.
+    if (
+      process.env.NEXT_TEST_PROXY_ADDRESS &&
+      // Validate that the proxy address is a valid IP address.
+      /^\d+\.\d+\.\d+\.\d+$/.test(process.env.NEXT_TEST_PROXY_ADDRESS)
+    ) {
+      this._writtenHostsLine = `${process.env.NEXT_TEST_PROXY_ADDRESS}\t${this._parsedUrl.hostname}\n`
+
+      require('console').log(
+        `Writing proxy address to hosts file: ${this._writtenHostsLine.trim()}`
+      )
+
+      // Using a child process, we'll use sudo to tee the hosts file to add the
+      // proxy address to the target domain.
+      await execa('sudo', ['tee', '-a', '/etc/hosts'], {
+        input: this._writtenHostsLine,
+        stdout: 'inherit',
+        shell: true,
+      })
+
+      // Verify that the proxy address was written to the hosts file.
+      const hostsFile = await fs.readFile('/etc/hosts', 'utf8')
+      if (!hostsFile.includes(this._writtenHostsLine)) {
+        throw new Error('Proxy address not found in hosts file after writing')
+      }
+
+      require('console').log(`Proxy address written to hosts file`)
+    }
+
     require('console').log(`Deployment URL: ${this._url}`)
     const buildIdUrl = `${this._url}${
       this.basePath || ''
@@ -168,6 +200,35 @@ export class NextDeployInstance extends NextInstance {
     this._cliOutput = buildLogs.stdout + buildLogs.stderr
   }
 
+  public async destroy() {
+    // If configured, we should remove the proxy address from the hosts file.
+    if (this._writtenHostsLine) {
+      const trimmed = this._writtenHostsLine.trim()
+
+      require('console').log(
+        `Removing proxy address from hosts file: ${this._writtenHostsLine.trim()}`
+      )
+
+      const hostsFile = await fs.readFile('/etc/hosts', 'utf8')
+
+      const cleanedHostsFile = hostsFile
+        .split('\n')
+        .filter((line) => line.trim() !== trimmed)
+        .join('\n')
+
+      await execa('sudo', ['tee', '/etc/hosts'], {
+        input: cleanedHostsFile,
+        stdout: 'inherit',
+        shell: true,
+      })
+
+      require('console').log(`Removed proxy address from hosts file`)
+    }
+
+    // Run the super destroy to clean up the test directory.
+    return super.destroy()
+  }
+
   public get cliOutput() {
     return this._cliOutput || ''
   }