diff --git a/.changeset/short-groups-knock.md b/.changeset/short-groups-knock.md new file mode 100644 index 0000000000..0b622a71ed --- /dev/null +++ b/.changeset/short-groups-knock.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli-common': patch +'@backstage/create-app': patch +'@backstage/cli': patch +--- + +Add proxy configuration from env-vars to create-app tasks diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index d8242fd325..edd81dbe5f 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -34,6 +34,10 @@ "start": "backstage-cli package start", "test": "backstage-cli package test" }, + "dependencies": { + "global-agent": "^3.0.0", + "undici": "^7.2.3" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/node": "^20.16.0" diff --git a/packages/cli-common/report.api.md b/packages/cli-common/report.api.md index 34f56c3f19..0e3c80afa4 100644 --- a/packages/cli-common/report.api.md +++ b/packages/cli-common/report.api.md @@ -6,6 +6,9 @@ // @public export const BACKSTAGE_JSON = 'backstage.json'; +// @public +export function bootstrapEnvProxyAgents(): void; + // @public export function findPaths(searchDir: string): Paths; diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index 8809764b43..11e5d05f4f 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 { bootstrapEnvProxyAgents } 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..cefa1bd36e --- /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 { bootstrapEnvProxyAgents } from './proxyBootstrap'; + +// Avoid mutating the global agents used in other tests +jest.mock('global-agent', () => ({ + bootstrap: jest.fn(), +})); +jest.mock('undici', () => ({ + setGlobalDispatcher: jest.fn(), + EnvHttpProxyAgent: jest.fn(), +})); + +describe('bootstrapEnvProxyAgents', () => { + 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'); + bootstrapEnvProxyAgents(); + + 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'); + bootstrapEnvProxyAgents(); + + 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'); + bootstrapEnvProxyAgents(); + + 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'); + bootstrapEnvProxyAgents(); + + 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'); + + bootstrapEnvProxyAgents(); + + 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'); + bootstrapEnvProxyAgents(); + + 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..b7375b6e55 --- /dev/null +++ b/packages/cli-common/src/proxyBootstrap.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +/** + * This function can be called to setup undici and node-fetch Proxy agents. + * + * You can set GLOBAL_AGENT_HTTP(S)_PROXY to configure a proxy to be used in the + * CLIs. + * + * You can also configure a custom namespace by setting + * GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE which will replace the default + * "GLOBAL_AGENT_" env-var prefix. + * + * Make sure to call this function before any other imports. + * + * @public + */ +export function bootstrapEnvProxyAgents() { + // 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 0d5e2f2f35..19dcaae28f 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BACKSTAGE_JSON, bootstrapEnvProxyAgents } from '@backstage/cli-common'; -maybeBootstrapProxy(); +bootstrapEnvProxyAgents(); import { env } from 'process'; import fs from 'fs-extra'; @@ -32,7 +33,6 @@ import { mapDependencies, YarnInfoInspectData, } from '../../../../lib/versioning'; -import { BACKSTAGE_JSON } from '@backstage/cli-common'; import { runParallelWorkers } from '../../../../lib/parallel'; import { getManifestByReleaseLine, @@ -43,26 +43,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/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 230590e99c..0ab31dd5f4 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { bootstrapEnvProxyAgents } from '@backstage/cli-common'; + +bootstrapEnvProxyAgents(); import chalk from 'chalk'; import fs from 'fs-extra'; diff --git a/yarn.lock b/yarn.lock index 46fb5cf160..a66ff596ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3141,6 +3141,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