From cdbec2e0d5673db61ec94116e239f1d7639c2b12 Mon Sep 17 00:00:00 2001 From: enyineer Date: Fri, 10 Oct 2025 10:09:47 +0200 Subject: [PATCH] Add proxy to create-app tasks Signed-off-by: enyineer --- packages/cli-common/src/index.ts | 1 + .../cli-common/src/proxyBootstrap.test.ts | 108 ++++++++++++++++++ packages/cli-common/src/proxyBootstrap.ts | 35 ++++++ .../modules/migrate/commands/versions/bump.ts | 22 +--- packages/create-app/package.json | 5 +- packages/create-app/src/lib/tasks.test.ts | 9 ++ packages/create-app/src/lib/tasks.ts | 21 +--- yarn.lock | 12 ++ 8 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 packages/cli-common/src/proxyBootstrap.test.ts create mode 100644 packages/cli-common/src/proxyBootstrap.ts diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index 8809764b43..194644db95 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -23,3 +23,4 @@ export { findPaths, BACKSTAGE_JSON } from './paths'; export { isChildPath } from './isChildPath'; export type { Paths, ResolveFunc } from './paths'; +export { maybeBootstrapProxy } from './proxyBootstrap'; diff --git a/packages/cli-common/src/proxyBootstrap.test.ts b/packages/cli-common/src/proxyBootstrap.test.ts new file mode 100644 index 0000000000..1132e640f0 --- /dev/null +++ b/packages/cli-common/src/proxyBootstrap.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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 { maybeBootstrapProxy } from './proxyBootstrap'; + +jest.mock('global-agent', () => ({ + bootstrap: jest.fn(), +})); + +jest.mock('undici', () => ({ + setGlobalDispatcher: jest.fn(), + EnvHttpProxyAgent: jest.fn(), +})); + +describe('maybeBootstrapProxy', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + jest.clearAllMocks(); + }); + + it('should bootstrap global-agent if GLOBAL_AGENT_HTTP_PROXY is set', () => { + process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + maybeBootstrapProxy(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); + + it('should bootstrap global-agent if GLOBAL_AGENT_HTTPS_PROXY is set', () => { + process.env.GLOBAL_AGENT_HTTPS_PROXY = 'https://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + maybeBootstrapProxy(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); + + it('should use undici EnvHttpProxyAgent if HTTP_PROXY is set', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com'; + + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + maybeBootstrapProxy(); + + expect(EnvHttpProxyAgent).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledWith( + expect.any(EnvHttpProxyAgent), + ); + }); + + it('should use undici EnvHttpProxyAgent if HTTPS_PROXY is set', () => { + process.env.HTTPS_PROXY = 'https://proxy.example.com'; + + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + maybeBootstrapProxy(); + + expect(EnvHttpProxyAgent).toHaveBeenCalledTimes(1); + expect(setGlobalDispatcher).toHaveBeenCalledWith( + expect.any(EnvHttpProxyAgent), + ); + }); + + it('should not bootstrap global-agent or set undici dispatcher if no proxy is set', () => { + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + const { setGlobalDispatcher } = + require('undici') as typeof import('undici'); + + maybeBootstrapProxy(); + + expect(bootstrap).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + }); + + it('should respect GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE', () => { + process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = 'CUSTOM_AGENT_'; + process.env.CUSTOM_AGENT_HTTP_PROXY = 'http://proxy.example.com'; + + const { bootstrap } = + require('global-agent') as typeof import('global-agent'); + maybeBootstrapProxy(); + + expect(bootstrap).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli-common/src/proxyBootstrap.ts b/packages/cli-common/src/proxyBootstrap.ts new file mode 100644 index 0000000000..6632a9054f --- /dev/null +++ b/packages/cli-common/src/proxyBootstrap.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Backstage Authors + * + * 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. + */ + +export function maybeBootstrapProxy() { + // see https://www.npmjs.com/package/global-agent + const globalAgentNamespace = + process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_'; + if ( + process.env[`${globalAgentNamespace}HTTP_PROXY`] || + process.env[`${globalAgentNamespace}HTTPS_PROXY`] + ) { + const globalAgent = + require('global-agent') as typeof import('global-agent'); + globalAgent.bootstrap(); + } + + if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) { + const { setGlobalDispatcher, EnvHttpProxyAgent } = + require('undici') as typeof import('undici'); + setGlobalDispatcher(new EnvHttpProxyAgent()); + } +} diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 39cfeddbd5..010b3de673 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BACKSTAGE_JSON, maybeBootstrapProxy } from '@backstage/cli-common'; maybeBootstrapProxy(); @@ -31,7 +32,6 @@ import { Lockfile, YarnInfoInspectData, } from '../../../../lib/versioning'; -import { BACKSTAGE_JSON } from '@backstage/cli-common'; import { runParallelWorkers } from '../../../../lib/parallel'; import { getManifestByReleaseLine, @@ -42,26 +42,6 @@ import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '../../../../lib/run'; -function maybeBootstrapProxy() { - // see https://www.npmjs.com/package/global-agent - const globalAgentNamespace = - process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_'; - if ( - process.env[`${globalAgentNamespace}HTTP_PROXY`] || - process.env[`${globalAgentNamespace}HTTPS_PROXY`] - ) { - const globalAgent = - require('global-agent') as typeof import('global-agent'); - globalAgent.bootstrap(); - } - - if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) { - const { setGlobalDispatcher, EnvHttpProxyAgent } = - require('undici') as typeof import('undici'); - setGlobalDispatcher(new EnvHttpProxyAgent()); - } -} - const DEP_TYPES = [ 'dependencies', 'devDependencies', diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 9fa8af4c1b..65d8801859 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -47,16 +47,19 @@ "chalk": "^4.0.0", "commander": "^12.0.0", "fs-extra": "^11.2.0", + "global-agent": "^3.0.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", "ora": "^5.3.0", - "recursive-readdir": "^2.2.2" + "recursive-readdir": "^2.2.2", + "undici": "^7.2.3" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^11.0.0", + "@types/global-agent": "^3", "@types/inquirer": "^8.1.3", "@types/node": "^20.16.0", "@types/recursive-readdir": "^2.2.0", diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 86a7008b4d..0556cbf1da 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -36,6 +36,15 @@ import { import { http, HttpResponse, delay } from 'msw'; import { setupServer } from 'msw/node'; +// Avoid mutating the global agents used in other tests +jest.mock('global-agent', () => ({ + bootstrap: jest.fn(), +})); +jest.mock('undici', () => ({ + setGlobalDispatcher: jest.fn(), + EnvHttpProxyAgent: class {}, +})); + jest.spyOn(Task, 'log').mockReturnValue(undefined); jest.spyOn(Task, 'error').mockReturnValue(undefined); jest.spyOn(Task, 'section').mockReturnValue(undefined); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 327094f7cd..1bd42a108a 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { maybeBootstrapProxy } from '@backstage/cli-common'; maybeBootstrapProxy(); @@ -32,26 +33,6 @@ import { packageVersions } from './versions'; import { promisify } from 'util'; import os from 'os'; -function maybeBootstrapProxy() { - // see https://www.npmjs.com/package/global-agent - const globalAgentNamespace = - process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE ?? 'GLOBAL_AGENT_'; - if ( - process.env[`${globalAgentNamespace}HTTP_PROXY`] || - process.env[`${globalAgentNamespace}HTTPS_PROXY`] - ) { - const globalAgent = - require('global-agent') as typeof import('global-agent'); - globalAgent.bootstrap(); - } - - if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY) { - const { setGlobalDispatcher, EnvHttpProxyAgent } = - require('undici') as typeof import('undici'); - setGlobalDispatcher(new EnvHttpProxyAgent()); - } -} - const TASK_NAME_MAX_LENGTH = 14; const TEN_MINUTES_MS = 1000 * 60 * 10; const exec = promisify(execCb); diff --git a/yarn.lock b/yarn.lock index 865c2a931e..58105e4d68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2802,6 +2802,8 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@types/node": "npm:^20.16.0" + global-agent: "npm:^3.0.0" + undici: "npm:^7.2.3" languageName: unknown linkType: soft @@ -3259,18 +3261,21 @@ __metadata: "@backstage/cli-common": "workspace:^" "@types/command-exists": "npm:^1.2.0" "@types/fs-extra": "npm:^11.0.0" + "@types/global-agent": "npm:^3" "@types/inquirer": "npm:^8.1.3" "@types/node": "npm:^20.16.0" "@types/recursive-readdir": "npm:^2.2.0" chalk: "npm:^4.0.0" commander: "npm:^12.0.0" fs-extra: "npm:^11.2.0" + global-agent: "npm:^3.0.0" handlebars: "npm:^4.7.3" inquirer: "npm:^8.2.0" msw: "npm:^2.0.0" nodemon: "npm:^3.0.1" ora: "npm:^5.3.0" recursive-readdir: "npm:^2.2.2" + undici: "npm:^7.2.3" bin: backstage-create-app: bin/backstage-create-app languageName: unknown @@ -19972,6 +19977,13 @@ __metadata: languageName: node linkType: hard +"@types/global-agent@npm:^3": + version: 3.0.0 + resolution: "@types/global-agent@npm:3.0.0" + checksum: 10/f961aaf493176e651dcb1bdb8f31b3f933f7ba86a74901c40399622e7f09621d84e0c12cca62ddee12bd8d783656359d25ec88aa57bba3fbcbd58f60f8cbdb18 + languageName: node + linkType: hard + "@types/google-protobuf@npm:^3.7.2": version: 3.15.12 resolution: "@types/google-protobuf@npm:3.15.12"