From 13823aa5fb801cd8287618a4d96c2be01f5f65d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:46:16 +0000 Subject: [PATCH 1/2] Initial plan From 6a15cdc20c1b3a42d396be9ffeffafc78ec0f91c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 04:59:36 +0000 Subject: [PATCH 2/2] feat: Add PREEVY_HOST and PREEVY_ENV_ID environment variables --- packages/core/src/compose/remote.ts | 30 +++++- .../core/src/compose/service-links.test.ts | 100 ++++++++++++++++++ packages/core/src/compose/service-links.ts | 28 ++++- site/docs/recipes/service-discovery.md | 26 +++-- 4 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/compose/service-links.test.ts diff --git a/packages/core/src/compose/remote.ts b/packages/core/src/compose/remote.ts index 486975d6..b4a79b92 100644 --- a/packages/core/src/compose/remote.ts +++ b/packages/core/src/compose/remote.ts @@ -24,10 +24,30 @@ export const fetchRemoteUserModel = async (connection: MachineConnection) => { const serviceLinkEnvVars = ( expectedServiceUrls: { name: string; port: number; url: string }[], -) => Object.fromEntries( - expectedServiceUrls - .map(({ name, port, url }) => [`PREEVY_BASE_URI_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), url]) -) + envId?: string, +): Record => { + const baseUriVars = Object.fromEntries( + expectedServiceUrls + .map(({ name, port, url }) => [`PREEVY_BASE_URI_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), url]) + ) + + const hostVars = Object.fromEntries( + expectedServiceUrls + .map(({ name, port, url }) => { + try { + const hostname = new URL(url).hostname + return [`PREEVY_HOST_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), hostname] + } catch (e) { + // If URL parsing fails, fallback to empty string + return [`PREEVY_HOST_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), ''] + } + }) + ) + + const envIdVars: Record = envId ? { PREEVY_ENV_ID: envId } : {} + + return { ...baseUriVars, ...hostVars, ...envIdVars } +} export const defaultVolumeSkipList: string[] = [ '/var/log', @@ -185,7 +205,7 @@ export const remoteComposeModel = async ({ log.debug(`Using compose files: ${composeFiles.files.join(', ')} and project directory "${composeFiles.projectDirectory}"`) - const linkEnvVars = serviceLinkEnvVars(expectedServiceUrls) + const linkEnvVars = serviceLinkEnvVars(expectedServiceUrls, agentSettings?.envId) const composeClientWithInjectedArgs = localComposeClient({ composeFiles: composeFiles.files, diff --git a/packages/core/src/compose/service-links.test.ts b/packages/core/src/compose/service-links.test.ts new file mode 100644 index 00000000..37ca6fd1 --- /dev/null +++ b/packages/core/src/compose/service-links.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from '@jest/globals' +import { serviceLinkEnvVars } from './service-links.js' + +describe('serviceLinkEnvVars', () => { + const mockServiceUrls = [ + { name: 'frontend', port: 3000, url: 'https://frontend-3000-env123-client456.livecycle.run/' }, + { name: 'api', port: 8080, url: 'https://api-8080-env123-client456.livecycle.run/' }, + { name: 'my-service', port: 9000, url: 'https://my-service-9000-env123-client456.livecycle.run/' }, + ] + + describe('backward compatibility', () => { + it('should generate PREEVY_BASE_URI variables for each service and port', () => { + const result = serviceLinkEnvVars(mockServiceUrls) + + expect(result).toMatchObject({ + 'PREEVY_BASE_URI_FRONTEND_3000': 'https://frontend-3000-env123-client456.livecycle.run/', + 'PREEVY_BASE_URI_API_8080': 'https://api-8080-env123-client456.livecycle.run/', + 'PREEVY_BASE_URI_MY_SERVICE_9000': 'https://my-service-9000-env123-client456.livecycle.run/', + }) + }) + + it('should normalize service names with non-alphanumeric characters', () => { + const serviceUrls = [ + { name: 'my-service-name', port: 3000, url: 'https://example.com/' }, + { name: 'service.with.dots', port: 8080, url: 'https://example.com/' }, + ] + + const result = serviceLinkEnvVars(serviceUrls) + + expect(result).toMatchObject({ + 'PREEVY_BASE_URI_MY_SERVICE_NAME_3000': 'https://example.com/', + 'PREEVY_BASE_URI_SERVICE_WITH_DOTS_8080': 'https://example.com/', + }) + }) + }) + + describe('new environment variables', () => { + it('should generate PREEVY_HOST variables with just the hostname', () => { + const result = serviceLinkEnvVars(mockServiceUrls) + + expect(result).toMatchObject({ + 'PREEVY_HOST_FRONTEND_3000': 'frontend-3000-env123-client456.livecycle.run', + 'PREEVY_HOST_API_8080': 'api-8080-env123-client456.livecycle.run', + 'PREEVY_HOST_MY_SERVICE_9000': 'my-service-9000-env123-client456.livecycle.run', + }) + }) + + it('should include PREEVY_ENV_ID when envId is provided', () => { + const result = serviceLinkEnvVars(mockServiceUrls, 'test-env-123') + + expect(result).toMatchObject({ + 'PREEVY_ENV_ID': 'test-env-123', + }) + }) + + it('should not include PREEVY_ENV_ID when envId is not provided', () => { + const result = serviceLinkEnvVars(mockServiceUrls) + + expect(result).not.toHaveProperty('PREEVY_ENV_ID') + }) + + it('should handle invalid URLs gracefully for host extraction', () => { + const serviceUrls = [ + { name: 'valid', port: 3000, url: 'https://valid.example.com/' }, + { name: 'invalid', port: 8080, url: 'not-a-url' }, + ] + + const result = serviceLinkEnvVars(serviceUrls) + + expect(result).toMatchObject({ + 'PREEVY_BASE_URI_VALID_3000': 'https://valid.example.com/', + 'PREEVY_BASE_URI_INVALID_8080': 'not-a-url', + 'PREEVY_HOST_VALID_3000': 'valid.example.com', + 'PREEVY_HOST_INVALID_8080': '', // Falls back to empty string for invalid URLs + }) + }) + }) + + describe('complete environment variable generation', () => { + it('should generate all expected environment variables when envId is provided', () => { + const result = serviceLinkEnvVars(mockServiceUrls, 'test-env-123') + + // Verify all PREEVY_BASE_URI variables + expect(result['PREEVY_BASE_URI_FRONTEND_3000']).toBe('https://frontend-3000-env123-client456.livecycle.run/') + expect(result['PREEVY_BASE_URI_API_8080']).toBe('https://api-8080-env123-client456.livecycle.run/') + expect(result['PREEVY_BASE_URI_MY_SERVICE_9000']).toBe('https://my-service-9000-env123-client456.livecycle.run/') + + // Verify all PREEVY_HOST variables + expect(result['PREEVY_HOST_FRONTEND_3000']).toBe('frontend-3000-env123-client456.livecycle.run') + expect(result['PREEVY_HOST_API_8080']).toBe('api-8080-env123-client456.livecycle.run') + expect(result['PREEVY_HOST_MY_SERVICE_9000']).toBe('my-service-9000-env123-client456.livecycle.run') + + // Verify PREEVY_ENV_ID + expect(result['PREEVY_ENV_ID']).toBe('test-env-123') + + // Verify the total number of variables + expect(Object.keys(result)).toHaveLength(7) // 3 BASE_URI + 3 HOST + 1 ENV_ID + }) + }) +}) \ No newline at end of file diff --git a/packages/core/src/compose/service-links.ts b/packages/core/src/compose/service-links.ts index fb9f9d60..c7e17117 100644 --- a/packages/core/src/compose/service-links.ts +++ b/packages/core/src/compose/service-links.ts @@ -1,6 +1,26 @@ export const serviceLinkEnvVars = ( expectedServiceUrls: { name: string; port: number; url: string }[], -) => Object.fromEntries( - expectedServiceUrls - .map(({ name, port, url }) => [`PREEVY_BASE_URI_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), url]) -) + envId?: string, +): Record => { + const baseUriVars = Object.fromEntries( + expectedServiceUrls + .map(({ name, port, url }) => [`PREEVY_BASE_URI_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), url]) + ) + + const hostVars = Object.fromEntries( + expectedServiceUrls + .map(({ name, port, url }) => { + try { + const hostname = new URL(url).hostname + return [`PREEVY_HOST_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), hostname] + } catch (e) { + // If URL parsing fails, fallback to empty string + return [`PREEVY_HOST_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${port}`.toUpperCase(), ''] + } + }) + ) + + const envIdVars: Record = envId ? { PREEVY_ENV_ID: envId } : {} + + return { ...baseUriVars, ...hostVars, ...envIdVars } +} diff --git a/site/docs/recipes/service-discovery.md b/site/docs/recipes/service-discovery.md index 2c98c225..3429b955 100644 --- a/site/docs/recipes/service-discovery.md +++ b/site/docs/recipes/service-discovery.md @@ -14,10 +14,12 @@ services: ports: 3000:80 ``` -Preevy will generate the following environment variable which will contain the generated preview environment URL: +Preevy will generate the following environment variables which will contain information about the generated preview environment: ```bash PREEVY_BASE_URI_SERVICE_NAME_3000=https://service-name-3000-envid-clientid.livecycle.run/ +PREEVY_HOST_SERVICE_NAME_3000=service-name-3000-envid-clientid.livecycle.run +PREEVY_ENV_ID=envid ``` ## Problem @@ -29,13 +31,19 @@ Service-to-service communication within containers can be handled using Docker C However, this method does not apply to code executed in the browser, which creates difficulties for frontend applications when connecting to backend services through exposed ports. The tunneling URL needs to be substituted, but it cannot be determined at build time. ## Solution -Preevy offers a simple solution for this problem by exposing the tunneling URL as an environment variable at Compose *build time*. Environment variables can be [interpolated](https://docs.docker.com/compose/compose-file/12-interpolation/) in the Compose file. +Preevy offers a simple solution for this problem by exposing tunneling information as environment variables at Compose *build time*. Environment variables can be [interpolated](https://docs.docker.com/compose/compose-file/12-interpolation/) in the Compose file. -The environment variable is named after the service name + port, with the prefix `PREEVY_BASE_URI`. For example, if the service name is `frontend` and is exposed on port 4000, the environment variable will be `PREEVY_BASE_URI_FRONTEND_4000`. +Preevy provides the following environment variables: -If the service is exposed on multiple ports, the environment variable will be created for each port. +- **`PREEVY_BASE_URI_{SERVICE}_{PORT}`**: The complete URL including protocol and trailing slash (e.g., `https://service-3000-envid-clientid.livecycle.run/`) +- **`PREEVY_HOST_{SERVICE}_{PORT}`**: Just the hostname without protocol or trailing slash (e.g., `service-3000-envid-clientid.livecycle.run`) +- **`PREEVY_ENV_ID`**: The environment ID (e.g., `envid`) -*Note about service name normalization*: Non-alphanumeric characters in service names are replaced by `_` (underscore) in the `PREEVY_BASE_URI` environment variable. E.g, the environment variable for service `my-service` at port 80 will be `PREEVY_BASE_URI_MY_SERVICE_80`. +The service-specific environment variables are named after the service name + port. For example, if the service name is `frontend` and is exposed on port 4000, the environment variables will be `PREEVY_BASE_URI_FRONTEND_4000` and `PREEVY_HOST_FRONTEND_4000`. + +If the service is exposed on multiple ports, environment variables will be created for each port. + +*Note about service name normalization*: Non-alphanumeric characters in service names are replaced by `_` (underscore) in the environment variable names. E.g, the environment variables for service `my-service` at port 80 will be `PREEVY_BASE_URI_MY_SERVICE_80` and `PREEVY_HOST_MY_SERVICE_80`. ## Example @@ -56,7 +64,7 @@ services: - 9006:3000 ``` -In this example, the frontend service is configured to communicate with the API service on port 9005. This works well in development, but when using Preevy, the port is not known in advance. To solve this, we can use the `PREEVY_BASE_URI` environment variable: +In this example, the frontend service is configured to communicate with the API service on port 9005. This works well in development, but when using Preevy, the port is not known in advance. To solve this, we can use the Preevy environment variables: ```yaml services: @@ -67,10 +75,14 @@ services: my-frontend: environment: - API_URL=${PREEVY_BASE_URI_MY_BACKEND_9006:-http://localhost:9006/} + # Or use just the hostname: + - API_HOST=${PREEVY_HOST_MY_BACKEND_9006:-localhost:9006} + # Or use the environment ID to build your own URL: + - ENV_ID=${PREEVY_ENV_ID:-local} my-backend: ... ports: - 9006:3000 ``` -To keep things working normally in local development, where the `PREEVY_BASE_URI` variables are not defined, a [default value](https://docs.docker.com/compose/compose-file/12-interpolation/) of `http://localhost:9006/` is given. +To keep things working normally in local development, where the Preevy variables are not defined, [default values](https://docs.docker.com/compose/compose-file/12-interpolation/) are provided.