Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAC proxy agent support #1839

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
"@babel/eslint-parser": "^7.14.7",
"@babel/preset-env": "^7.14.7",
"@babel/register": "^7.17.7",
"babel-plugin-transform-import-meta": "^2.2.1",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"babel-plugin-istanbul": "^7.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-import-meta": "^2.2.1",
"cross-env": "^7.0.2",
"eslint": "^7.30.0",
"eslint-config-standard": "^16.0.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-build/test/finalize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ describe('percy build:finalize', () => {

it('defaults PERCY_PARALLEL_TOTAL to -1', async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';

process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
expect(process.env.PERCY_PARALLEL_TOTAL).toBeUndefined();
await finalize();
expect(process.env.PERCY_PARALLEL_TOTAL).toEqual('-1');
});

it('gets parallel build info and finalizes all parallel builds', async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await finalize();

expect(logger.stderr).toEqual([]);
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-build/test/wait.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ describe('percy build:wait', () => {

beforeEach(async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest({ loggerTTY: true });
});

afterEach(() => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_ENABLE;
delete process.env.PERCY_FORCE_PKG_VALUE;
});

it('does nothing and logs when percy is not enabled', async () => {
Expand Down Expand Up @@ -232,7 +234,6 @@ describe('percy build:wait', () => {
})]);

await expectAsync(wait(['--build=123'])).toBeRejected();

expect(logger.stdout).toEqual([]);
expect(logger.stderr).toEqual(jasmine.arrayContaining([
'[percy] Build #10 failed! https://percy.io/test/test/123',
Expand Down
7 changes: 7 additions & 0 deletions packages/cli-exec/test/exec.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import exec from '@percy/cli-exec';
describe('percy exec', () => {
beforeEach(async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
jasmine.DEFAULT_TIMEOUT_INTERVAL = 25000;
await setupTest();

let { default: which } = await import('which');
spyOn(which, 'sync').and.callFake(c => c);
spyOn(process, 'exit').and.callFake(c => c);
process.env.PERCY_CLIENT_ERROR_LOGS = false;

// Ensure global.__MOCK_IMPORTS__ is defined
global.__MOCK_IMPORTS__ = global.__MOCK_IMPORTS__ || new Map();
});

afterEach(() => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete process.env.PERCY_ENABLE;
delete process.env.PERCY_BUILD_ID;
delete process.env.PERCY_PARALLEL_TOTAL;
Expand Down Expand Up @@ -243,9 +248,11 @@ describe('percy exec', () => {
let [e, err] = [new EventEmitter(), new Error('spawn error')];
let crossSpawn = () => (setImmediate(() => e.emit('error', err)), e);
global.__MOCK_IMPORTS__.set('cross-spawn', { default: crossSpawn });

let stdinSpy = spyOn(process.stdin, 'pipe').and.resolveTo('some response');

await expectAsync(exec(['--', 'foobar'])).toBeRejected();

expect(stdinSpy).toHaveBeenCalled();
console.log(logger.stderr);
expect(logger.stderr).toEqual(jasmine.arrayContaining([
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-exec/test/ping.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ describe('percy exec:ping', () => {

beforeEach(async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest();
});

afterEach(async () => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete process.env.PERCY_ENABLE;
delete process.env.PERCY_PARALLEL_TOTAL;
delete process.env.PERCY_PARTIAL_BUILD;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-exec/test/start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('percy exec:start', () => {

beforeEach(async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest();

started = start(['--quiet']);
Expand Down Expand Up @@ -117,6 +118,7 @@ describe('percy exec:start', () => {
logger.reset();

process.env.PERCY_ENABLE = '0';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await start();

expect(logger.stdout).toEqual([]);
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-exec/test/stop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ describe('percy exec:stop', () => {

beforeEach(async () => {
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest();
});

afterEach(async () => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete process.env.PERCY_ENABLE;
delete process.env.PERCY_PARALLEL_TOTAL;
delete process.env.PERCY_PARTIAL_BUILD;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-snapshot/test/directory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe('percy snapshot <directory>', () => {
beforeEach(async () => {
snapshot.packageInformation = { name: '@percy/cli-snapshot' };
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });

await setupTest({
filesystem: {
Expand All @@ -21,6 +22,7 @@ describe('percy snapshot <directory>', () => {

afterEach(() => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete process.env.PERCY_ENABLE;
delete snapshot.packageInformation;
delete process.env.PERCY_CLIENT_ERROR_LOGS;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-snapshot/test/file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('percy snapshot <file>', () => {
beforeEach(async () => {
snapshot.packageInformation = { name: '@percy/cli-snapshot' };
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });

server = await createTestServer({
default: () => [200, 'text/html', '<p>Test</p>']
Expand Down Expand Up @@ -44,6 +45,7 @@ describe('percy snapshot <file>', () => {

afterEach(async () => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete process.env.PERCY_CLIENT_ERROR_LOGS;
delete snapshot.packageInformation;
await server.close();
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-snapshot/test/sitemap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('percy snapshot <sitemap>', () => {
beforeEach(async () => {
snapshot.packageInformation = { name: '@percy/cli-snapshot' };
process.env.PERCY_TOKEN = '<<PERCY_TOKEN>>';
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest();

server = await createTestServer({
Expand All @@ -33,6 +34,7 @@ describe('percy snapshot <sitemap>', () => {

afterEach(async () => {
delete process.env.PERCY_TOKEN;
delete process.env.PERCY_FORCE_PKG_VALUE;
delete snapshot.packageInformation;
await server.close();
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli-upload/test/upload.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('percy upload', () => {
upload.packageInformation = { name: '@percy/cli-upload' };
process.env.PERCY_TOKEN = 'web_<<PERCY_TOKEN>>';
process.env.PERCY_CLIENT_ERROR_LOGS = false;
process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
await setupTest({
filesystem: {
'images/test-1.png': pixel,
Expand Down
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@percy/env": "1.30.6",
"@percy/logger": "1.30.6",
"pac-proxy-agent": "^7.0.2",
"pako": "^2.1.0"
}
}
6 changes: 5 additions & 1 deletion packages/client/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {

// Default client API URL can be set with an env var for API development
const { PERCY_CLIENT_API_URL = 'https://percy.io/api/v1' } = process.env;
const pkg = getPackageJSON(import.meta.url);
let pkg = getPackageJSON(import.meta.url);
// minimum polling interval milliseconds
const MIN_POLLING_INTERVAL = 1_000;
const INVALID_TOKEN_ERROR_MESSAGE = 'Unable to retrieve snapshot details with write access token. Kindly use a full access token for retrieving snapshot details with Synchronous CLI.';
Expand Down Expand Up @@ -83,6 +83,10 @@ export class PercyClient {

// Stringifies client and environment info.
userAgent() {
// forcedPkgValue has been added since when percy package is bundled inside Electron app (LCNC)
// we can't read Percy's package json for package name and version, so we are passing it via env variables
if (this.env.forcedPkgValue) pkg = this.env.forcedPkgValue;

let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`]
.concat(`${pkg.name}/${pkg.version}`, ...this.clientInfo)
.filter(Boolean));
Expand Down
48 changes: 43 additions & 5 deletions packages/client/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ import http from 'http';
import https from 'https';
import logger from '@percy/logger';
import { stripQuotesAndSpaces } from '@percy/env/utils';
import { PacProxyAgent } from 'pac-proxy-agent';

const CRLF = '\r\n';
const STATUS_REG = /^HTTP\/1.[01] (\d*)/;

// function to create PAC proxy agent
export function createPacAgent(pacUrl, options = {}) {
pacUrl = stripQuotesAndSpaces(pacUrl);
try {
const agent = new PacProxyAgent(pacUrl, {
keepAlive: true,
...options
});

logger('client:proxy').info(`Successfully loaded PAC file from: ${pacUrl}`);
return agent;
} catch (error) {
logger('client:proxy').error(`Failed to load PAC file, error message: ${error.message}, stack: ${error.stack}`);
throw new Error(`Failed to initialize PAC proxy: ${error.message}`);
}
}

// Returns true if the URL hostname matches any patterns
export function hostnameMatches(patterns, url) {
let subject = new URL(url);
Expand Down Expand Up @@ -219,11 +237,31 @@ export function proxyAgentFor(url, options) {
let { protocol, hostname } = new URL(url);
let cachekey = `${protocol}//${hostname}`;

if (!cache.has(cachekey)) {
cache.set(cachekey, protocol === 'https:'
? new ProxyHttpsAgent(options)
: new ProxyHttpAgent(options));
// If we already have a cached agent, return it
if (cache.has(cachekey)) {
return cache.get(cachekey);
}

return cache.get(cachekey);
try {
let agent;
const pacUrl = process.env.PERCY_PAC_FILE_URL;

// If PAC URL is provided, use PAC proxy
if (pacUrl) {
logger('client:proxy').info(`Using PAC file from: ${pacUrl}`);
agent = createPacAgent(pacUrl, options);
} else {
// Fall back to other proxy configuration
agent = protocol === 'https:'
? new ProxyHttpsAgent(options)
: new ProxyHttpAgent(options);
}

// Cache the created agent
cache.set(cachekey, agent);
return agent;
} catch (error) {
logger('client:proxy').error(`Failed to create proxy agent: ${error.message}`);
throw error;
}
}
4 changes: 3 additions & 1 deletion packages/client/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export async function request(url, options = {}, callback) {
let { protocol, hostname, port, pathname, search, hash } = new URL(url);

// reference the default export so tests can mock it
let { default: http } = await import(protocol === 'https:' ? 'https' : 'http');
// bundling cli inside electron or another package fails if we import it
// like this: await import(protocol === 'https:' ? 'https' : 'http');
let { default: http } = protocol === 'https:' ? await import('https') : await import('http');
let { proxyAgentFor } = await import('./proxy.js');

// automatically stringify body content
Expand Down
23 changes: 21 additions & 2 deletions packages/client/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ describe('PercyClient', () => {
await logger.mock({ level: 'debug' });
await api.mock();
delete process.env.PERCY_GZIP;

process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
client = new PercyClient({
token: 'PERCY_TOKEN'
});
});

describe('#userAgent()', () => {
it('uses default package value when env.forcedPkgValue is not set', () => {
delete process.env.PERCY_FORCE_PKG_VALUE;
client = new PercyClient({ token: 'PERCY_TOKEN' });

expect(client.userAgent()).toMatch(
/^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/
);
});
it('contains client and environment information', () => {
expect(client.userAgent()).toMatch(
/^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/
Expand All @@ -38,7 +46,7 @@ describe('PercyClient', () => {
expect(client.userAgent()).toMatch(
/^Percy\/v1 @percy\/client\/\S+ client-info \(env-info; node\/v[\d.]+.*\)$/
);
expect(logger.stderr.length).toEqual(2);
expect(logger.stderr.length).toBeGreaterThanOrEqual(2);
});

it('it logs a debug warning when no info is passed', async () => {
Expand Down Expand Up @@ -98,6 +106,17 @@ describe('PercyClient', () => {
/^Percy\/v1 @percy\/client\/\S+ client-info \(env-info; node\/v[\d.]+.*\)$/
);
});

it('uses forced package value when set', () => {
client.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
client = new PercyClient({
token: 'PERCY_TOKEN'
});
client.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' });
expect(client.userAgent()).toMatch(
/^Percy\/v1 @percy\/client\/1.0.0 \(node\/v[\d.]+.*\)$/
);
});
});

describe('#get()', () => {
Expand Down
Loading
Loading