From 8f715e612c781f28fce9acc6097333dbf88668ad Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Sat, 17 Jan 2026 13:58:32 -0800 Subject: [PATCH 1/4] feat: support envLens for github and docker types --- docs/schema/yaml/1.0.0.yaml | 4 + .../007_add_envlens_to_deployables.ts | 29 +++++ src/server/lib/jsonschema/schemas/1.0.0.json | 118 ++++++++++++++---- .../yamlSchemas/schema_1_0_0/schema_1_0_0.ts | 2 + src/server/models/Deployable.ts | 1 + src/server/models/yaml/YamlService.ts | 15 +++ src/server/services/build.ts | 36 ++++-- src/server/services/deployable.ts | 2 + 8 files changed, 172 insertions(+), 35 deletions(-) create mode 100644 src/server/db/migrations/007_add_envlens_to_deployables.ts diff --git a/docs/schema/yaml/1.0.0.yaml b/docs/schema/yaml/1.0.0.yaml index 31d6031..def8232 100644 --- a/docs/schema/yaml/1.0.0.yaml +++ b/docs/schema/yaml/1.0.0.yaml @@ -528,6 +528,8 @@ services: # @param services.github.deployment.node_affinity node_affinity: + # @param services.github.envLens + envLens: false # @param services.docker docker: # @param services.docker.dockerImage (required) @@ -667,6 +669,8 @@ services: # @param services.docker.deployment.node_affinity node_affinity: + # @param services.docker.envLens + envLens: false # @param services.externalHttp externalHttp: # @param services.externalHttp.defaultInternalHostname (required) diff --git a/src/server/db/migrations/007_add_envlens_to_deployables.ts b/src/server/db/migrations/007_add_envlens_to_deployables.ts new file mode 100644 index 0000000..c51c303 --- /dev/null +++ b/src/server/db/migrations/007_add_envlens_to_deployables.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2025 GoodRx, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('deployables', (table) => { + table.boolean('envLens').nullable().defaultTo(false); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('deployables', (table) => { + table.dropColumn('envLens'); + }); +} diff --git a/src/server/lib/jsonschema/schemas/1.0.0.json b/src/server/lib/jsonschema/schemas/1.0.0.json index 1751be9..da487a4 100644 --- a/src/server/lib/jsonschema/schemas/1.0.0.json +++ b/src/server/lib/jsonschema/schemas/1.0.0.json @@ -44,7 +44,9 @@ "type": "number" } }, - "required": ["name"] + "required": [ + "name" + ] } }, "optionalServices": { @@ -67,7 +69,9 @@ "type": "number" } }, - "required": ["name"] + "required": [ + "name" + ] } }, "webhooks": { @@ -120,7 +124,9 @@ "type": "number" } }, - "required": ["image"] + "required": [ + "image" + ] }, "command": { "type": "object", @@ -136,13 +142,20 @@ "type": "number" } }, - "required": ["image", "script"] + "required": [ + "image", + "script" + ] }, "env": { "type": "object" } }, - "required": ["state", "type", "env"] + "required": [ + "state", + "type", + "env" + ] } } } @@ -174,7 +187,9 @@ "type": "string" } }, - "required": ["name"] + "required": [ + "name" + ] } }, "deploymentDependsOn": { @@ -292,7 +307,9 @@ } } }, - "required": ["name"] + "required": [ + "name" + ] }, "envLens": { "type": "boolean" @@ -364,7 +381,9 @@ "minItems": 1 } }, - "required": ["dockerfilePath"] + "required": [ + "dockerfilePath" + ] }, "init": { "type": "object", @@ -383,10 +402,15 @@ "type": "object" } }, - "required": ["dockerfilePath"] + "required": [ + "dockerfilePath" + ] } }, - "required": ["defaultTag", "app"] + "required": [ + "defaultTag", + "app" + ] } } }, @@ -630,7 +654,11 @@ "type": "string" } }, - "required": ["name", "mountPath", "storageSize"] + "required": [ + "name", + "mountPath", + "storageSize" + ] } }, "node_selector": { @@ -646,7 +674,10 @@ } } }, - "required": ["repository", "branchName"] + "required": [ + "repository", + "branchName" + ] }, "github": { "type": "object", @@ -716,7 +747,9 @@ "minItems": 1 } }, - "required": ["dockerfilePath"] + "required": [ + "dockerfilePath" + ] }, "init": { "type": "object", @@ -735,10 +768,15 @@ "type": "object" } }, - "required": ["dockerfilePath"] + "required": [ + "dockerfilePath" + ] } }, - "required": ["defaultTag", "app"] + "required": [ + "defaultTag", + "app" + ] }, "deployment": { "type": "object", @@ -943,7 +981,11 @@ "type": "string" } }, - "required": ["name", "mountPath", "storageSize"] + "required": [ + "name", + "mountPath", + "storageSize" + ] } }, "node_selector": { @@ -957,9 +999,16 @@ "description": "Full Kubernetes nodeAffinity object for advanced node selection" } } + }, + "envLens": { + "type": "boolean" } }, - "required": ["repository", "branchName", "docker"] + "required": [ + "repository", + "branchName", + "docker" + ] }, "docker": { "type": "object", @@ -1186,7 +1235,11 @@ "type": "string" } }, - "required": ["name", "mountPath", "storageSize"] + "required": [ + "name", + "mountPath", + "storageSize" + ] } }, "node_selector": { @@ -1200,9 +1253,15 @@ "description": "Full Kubernetes nodeAffinity object for advanced node selection" } } + }, + "envLens": { + "type": "boolean" } }, - "required": ["dockerImage", "defaultTag"] + "required": [ + "dockerImage", + "defaultTag" + ] }, "externalHttp": { "type": "object", @@ -1215,7 +1274,10 @@ "type": "string" } }, - "required": ["defaultInternalHostname", "defaultPublicUrl"] + "required": [ + "defaultInternalHostname", + "defaultPublicUrl" + ] }, "auroraRestore": { "type": "object", @@ -1228,7 +1290,10 @@ "type": "string" } }, - "required": ["command", "arguments"] + "required": [ + "command", + "arguments" + ] }, "configuration": { "type": "object", @@ -1241,11 +1306,16 @@ "type": "string" } }, - "required": ["defaultTag", "branchName"] + "required": [ + "defaultTag", + "branchName" + ] } }, - "required": ["name"] + "required": [ + "name" + ] } } } -} +} \ No newline at end of file diff --git a/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts b/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts index 5d953d3..2e8637a 100644 --- a/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts +++ b/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts @@ -161,6 +161,7 @@ const schema_1_0_0 = { branchName: { type: 'string' }, docker, deployment, + envLens: { type: 'boolean' }, }, required: ['repository', 'branchName', 'docker'], }, @@ -175,6 +176,7 @@ const schema_1_0_0 = { env: { type: 'object' }, ports: { type: 'array' }, deployment, + envLens: { type: 'boolean' }, }, required: ['dockerImage', 'defaultTag'], }, diff --git a/src/server/models/Deployable.ts b/src/server/models/Deployable.ts index 4ddc040..c6e6bf9 100644 --- a/src/server/models/Deployable.ts +++ b/src/server/models/Deployable.ts @@ -37,6 +37,7 @@ export default class Deployable extends Service { deploymentDependsOn: string[]; kedaScaleToZero: KedaScaleToZero; builder: Builder; + envLens?: boolean; static relationMappings = { environment: { diff --git a/src/server/models/yaml/YamlService.ts b/src/server/models/yaml/YamlService.ts index 80a1902..19d486c 100644 --- a/src/server/models/yaml/YamlService.ts +++ b/src/server/models/yaml/YamlService.ts @@ -80,6 +80,7 @@ export interface GithubService extends Service { readonly init?: InitDockerConfig; }; readonly deployment?: DeploymentConfig; + readonly envLens?: boolean; }; } @@ -119,6 +120,7 @@ export interface DockerServiceConfig { readonly env?: Record; readonly ports?: number[]; readonly deployment?: DeploymentConfig; + readonly envLens?: boolean; } export interface CodefreshService extends Service { @@ -911,6 +913,19 @@ export function getBuilder(service: Service): Builder { return {}; } +export function getEnvLens(service: Service): boolean { + switch (getDeployType(service)) { + case DeployTypes.GITHUB: + return (service as GithubService)?.github?.envLens ?? false; + case DeployTypes.DOCKER: + return (service as DockerService)?.docker?.envLens ?? false; + case DeployTypes.HELM: + return (service as unknown as HelmService)?.helm?.envLens ?? false; + default: + return false; + } +} + export async function getEcr(service: Service): Promise { const svc = service as unknown as HelmService; const ecr = svc?.helm?.docker?.ecr; diff --git a/src/server/services/build.ts b/src/server/services/build.ts index 9a17a3e..90057bd 100644 --- a/src/server/services/build.ts +++ b/src/server/services/build.ts @@ -19,6 +19,7 @@ import * as k8s from 'server/lib/kubernetes'; import * as cli from 'server/lib/cli'; import * as github from 'server/lib/github'; import { uninstallHelmReleases } from 'server/lib/helm'; +import { ingressBannerSnippet } from 'server/lib/helm/utils'; import { customAlphabet, nanoid } from 'nanoid'; import { BuildEnvironmentVariables } from 'server/lib/buildEnvVariables'; @@ -285,9 +286,22 @@ export default class BuildService extends BaseService { * @param deploy */ private async ingressConfigurationForDeploy(deploy: Deploy): Promise { - await deploy.$fetchGraph('[build, service, deployable]'); + await deploy.$fetchGraph('[build.[pullRequest], service, deployable]'); const { build, service, deployable } = deploy; + if (!deployable) { + throw new Error(`Deployable not found for deploy ${deploy.uuid}`); + } + + const getIngressAnnotations = (baseAnnotations: Record | undefined): Record => { + if (build?.enableFullYaml && deployable.envLens) { + const bannerSnippet = ingressBannerSnippet(deploy); + const bannerAnnotation = bannerSnippet.metadata?.annotations || {}; + return { ...(baseAnnotations || {}), ...bannerAnnotation }; + } + return baseAnnotations || {}; + }; + if (build?.enableFullYaml) { if (deployable.hostPortMapping && Object.keys(deployable.hostPortMapping).length > 0) { return Object.keys(deployable.hostPortMapping).map((key) => { @@ -295,22 +309,22 @@ export default class BuildService extends BaseService { host: `${key}-${this.db.services.Deploy.hostForDeployableDeploy(deploy, deployable)}`, deployUUID: `${key}-${deploy.uuid}`, serviceHost: `${deploy.uuid}`, - ipWhitelist: deploy.deployable.ipWhitelist, - ingressAnnotations: deploy.deployable.ingressAnnotations, + ipWhitelist: deployable.ipWhitelist, + ingressAnnotations: getIngressAnnotations(deployable.ingressAnnotations), pathPortMapping: { - '/': parseInt(deploy.deployable.hostPortMapping[key], 10), + '/': parseInt(deployable.hostPortMapping[key], 10), }, }; }); - } else if (deploy.deployable.pathPortMapping && Object.keys(deploy.deployable.pathPortMapping).length > 0) { + } else if (deployable.pathPortMapping && Object.keys(deployable.pathPortMapping).length > 0) { return [ { host: `${this.db.services.Deploy.hostForDeployableDeploy(deploy, deployable)}`, deployUUID: `${deploy.uuid}`, serviceHost: `${deploy.uuid}`, - ipWhitelist: deploy.deployable.ipWhitelist, - ingressAnnotations: deploy.deployable.ingressAnnotations, - pathPortMapping: deploy.deployable.pathPortMapping, + ipWhitelist: deployable.ipWhitelist, + ingressAnnotations: getIngressAnnotations(deployable.ingressAnnotations), + pathPortMapping: deployable.pathPortMapping, }, ]; } else { @@ -319,10 +333,10 @@ export default class BuildService extends BaseService { host: this.db.services.Deploy.hostForDeployableDeploy(deploy, deployable), deployUUID: deploy.uuid, serviceHost: `${deploy.uuid}`, - ipWhitelist: deploy.deployable.ipWhitelist, - ingressAnnotations: deploy.deployable.ingressAnnotations, + ipWhitelist: deployable.ipWhitelist, + ingressAnnotations: getIngressAnnotations(deployable.ingressAnnotations), pathPortMapping: { - '/': parseInt(deploy.deployable.port, 10), + '/': parseInt(deployable.port, 10), }, }, ]; diff --git a/src/server/services/deployable.ts b/src/server/services/deployable.ts index 8248e9e..c72c619 100644 --- a/src/server/services/deployable.ts +++ b/src/server/services/deployable.ts @@ -94,6 +94,7 @@ export interface DeployableAttributes { deploymentDependsOn?: string[]; kedaScaleToZero?: KedaScaleToZero; builder?: Builder; + envLens?: boolean; nodeSelector?: Record; nodeAffinity?: Record; } @@ -376,6 +377,7 @@ export default class DeployableService extends BaseService { deploymentDependsOn: service.deploymentDependsOn || [], kedaScaleToZero: kedaScaleToZero?.enabled ? YamlService.getScaleToZeroConfig(service) : null, builder: YamlService.getBuilder(service) ?? {}, + envLens: YamlService.getEnvLens(service), }; } } catch (error) { From 65689d167f21a04c7e531bf3ec372ff2cd4505ff Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Sun, 18 Jan 2026 09:05:48 -0800 Subject: [PATCH 2/4] test: update for envLens --- src/server/services/__tests__/deployable.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/services/__tests__/deployable.test.ts b/src/server/services/__tests__/deployable.test.ts index 3c02db3..09e9d8b 100644 --- a/src/server/services/__tests__/deployable.test.ts +++ b/src/server/services/__tests__/deployable.test.ts @@ -160,6 +160,7 @@ describe('Deployable Service', () => { SOURCE: 'yaml', TOKEN1: 'abcdefghijk', }, + envLens: false, port: '8080,8089,8888', initArguments: '-c%%SPLIT%%local%%SPLIT%%-i%%SPLIT%%./sysops/ansible/spinnaker_inventory.py%%SPLIT%%./sysops/ansible/playbooks/lifecycle.yaml', @@ -323,6 +324,7 @@ describe('Deployable Service', () => { SOURCE: 'yaml', TOKEN1: 'abcdefghijk', }, + envLens: false, port: '8080,8089,8888', initArguments: '-c%%SPLIT%%local%%SPLIT%%-i%%SPLIT%%./sysops/ansible/spinnaker_inventory.py%%SPLIT%%./sysops/ansible/playbooks/lifecycle.yaml', From 1c57c523e07335106c16e4e6c253452745d101d3 Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Sun, 18 Jan 2026 10:11:12 -0800 Subject: [PATCH 3/4] fix: tracer errors in localdev --- src/pages/api/webhooks/github.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/webhooks/github.ts b/src/pages/api/webhooks/github.ts index 2d43293..b890dad 100644 --- a/src/pages/api/webhooks/github.ts +++ b/src/pages/api/webhooks/github.ts @@ -39,7 +39,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { const isBot = sender?.includes('[bot]') === true; if (event === 'issue_comment' && isBot) { - tracer.scope().active()?.setTag('manual.drop', true); + tracer.scope?.()?.active?.()?.setTag('manual.drop', true); res.status(200).end(); return; } From ae9ac4f4025d5ef681957c1ad42a5dcb1e36249b Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Sun, 18 Jan 2026 10:18:10 -0800 Subject: [PATCH 4/4] fix: fetch config from current repo fetch config from current repo on pushes but not from remote repos --- src/server/services/build.ts | 15 ++++++++++----- src/server/services/deployable.ts | 22 +++++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/server/services/build.ts b/src/server/services/build.ts index 90057bd..4613a71 100644 --- a/src/server/services/build.ts +++ b/src/server/services/build.ts @@ -436,11 +436,18 @@ export default class BuildService extends BaseService { } } - private async importYamlConfigFile(environment: Environment, build: Build) { + private async importYamlConfigFile(environment: Environment, build: Build, filterGithubRepositoryId?: number) { // Write the deployables here for now and not going to use them yet. try { const buildId = build?.id; - await this.db.services.Deployable.upsertDeployables(buildId, build.uuid, build.pullRequest, environment, build); + await this.db.services.Deployable.upsertDeployables( + buildId, + build.uuid, + build.pullRequest, + environment, + build, + filterGithubRepositoryId + ); await this.db.services.Webhook.upsertWebhooksWithYaml(build, build.pullRequest); } catch (error) { @@ -1286,9 +1293,7 @@ export default class BuildService extends BaseService { await build?.$fetchGraph('[pullRequest, environment]'); await build.pullRequest.$fetchGraph('[repository]'); - if (!githubRepositoryId) { - await this.importYamlConfigFile(build?.environment, build); - } + await this.importYamlConfigFile(build?.environment, build, githubRepositoryId); await this.db.services.BuildService.resolveAndDeployBuild( build, diff --git a/src/server/services/deployable.ts b/src/server/services/deployable.ts index c72c619..b914369 100644 --- a/src/server/services/deployable.ts +++ b/src/server/services/deployable.ts @@ -755,7 +755,8 @@ export default class DeployableService extends BaseService { buildId: number, buildUUID: string, pullRequest: PullRequest, - build?: Build + build?: Build, + filterGithubRepositoryId?: number ) { try { const attribution = async ( @@ -824,6 +825,13 @@ export default class DeployableService extends BaseService { let deploy: Deploy; // Service defined in remote repo. Need to fetch remote YAML if (yamlEnvService?.repository != null) { + // Skip remote dependency YAML fetches when filtering by specific repository + if (filterGithubRepositoryId) { + getLogger({ buildUUID, service: yamlEnvService.name }).debug( + 'Skipping remote YAML fetch for filtered deploy' + ); + return; + } // If the dependency service does not have a branch name defined use 'main' as the default branch name. branchName = yamlEnvService?.branch ?? 'main'; // Check if the deployable has a commentBranchName which is set in the lifecycle comment. If it does @@ -986,7 +994,8 @@ export default class DeployableService extends BaseService { buildUUID: string, pullRequest: PullRequest, environment: Environment, - build?: Build + build?: Build, + filterGithubRepositoryId?: number ): Promise { // We are going to ingest all the database and yaml configuration and process in the memory before writes into the database let deployables: Deployable[]; @@ -1011,7 +1020,14 @@ export default class DeployableService extends BaseService { // Next read the YAML config file from the PR's repository and branch // Overwrite the db config exists in the YAML + any YAML only configurations - await this.updateOrCreateDeployableUsingYamlConfig(deployableServices, buildId, buildUUID, pullRequest, build); + await this.updateOrCreateDeployableUsingYamlConfig( + deployableServices, + buildId, + buildUUID, + pullRequest, + build, + filterGithubRepositoryId + ); // Finally, Upsert the deployables into the database deployables = await this.upsertDeployablesWithDatabase(