From 28c24205ad360c567f7277b88dc235cf5877ce99 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Wed, 10 Jun 2026 14:17:06 +0100 Subject: [PATCH 1/5] fix: fix cf-pages gh pkgs sdk install by storing npm creds in local temp file --- tools/scripts/install.js | 61 +++++++++++++--- tools/scripts/install.test.mjs | 124 +++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 tools/scripts/install.test.mjs diff --git a/tools/scripts/install.js b/tools/scripts/install.js index 95e9687645..66d2bbb60f 100644 --- a/tools/scripts/install.js +++ b/tools/scripts/install.js @@ -1,5 +1,6 @@ const { execSync } = require('child_process') const fs = require('fs') +const os = require('os') const path = require('path') const ROOT_DIR = path.resolve(__dirname, '../..') @@ -189,10 +190,10 @@ function stripLockfileEntries(keys) { * Runs pnpm install in the repository root. * * Uses a frozen lockfile by default. When `authToken` is provided (SDK preview install) - * it injects the GitHub Packages registry/auth (and an optional rewrite pnpmfile) into - * the child pnpm process via `npm_config_*` env vars, then runs with - * `--no-frozen-lockfile`. The auth token never touches the filesystem, so a crash, - * SIGKILL, or Ctrl-C cannot leave a secret in the tracked `.npmrc`. + * it injects the GitHub Packages registry/auth through a temporary npmrc userconfig + * (and an optional rewrite pnpmfile) into the child pnpm process, then runs with + * `--no-frozen-lockfile`. The auth token is written only to an OS temp directory + * npmrc with restrictive permissions and is removed after the install attempt. */ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { if (!authToken) { @@ -211,13 +212,7 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { console.log('[install.js] Installing with SDK PR version (pnpm install --no-frozen-lockfile)...') - // pnpm follows the npm convention of reading config from `npm_config_` env vars. - // Passing the registry/auth this way keeps the token off disk entirely. - const childEnv = { - ...process.env, - 'npm_config_@cowprotocol:registry': 'https://npm.pkg.github.com', - 'npm_config_//npm.pkg.github.com/:_authToken': authToken, - } + const childEnv = { ...process.env } if (hasRewrites) { // `pnpmfile` is a pnpm-specific config key (no secret); pnpm resolves it relative to cwd. @@ -225,8 +220,13 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { } let originalLockfile = null + let tempNpmrcDir = null try { + const tempNpmrc = createTempNpmrc(authToken) + tempNpmrcDir = tempNpmrc.dir + childEnv.npm_config_userconfig = tempNpmrc.path + if (hasRewrites) createTempPnpmfile(rewriteMap) if (hasForceResolve) originalLockfile = stripLockfileEntries(forceResolveKeys) @@ -253,9 +253,48 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { console.warn('[install.js] Failed to restore pnpm-lock.yaml:', err) } } + if (tempNpmrcDir !== null) { + try { + removeTempNpmrc(tempNpmrcDir) + } catch (err) { + console.warn('[install.js] Failed to remove temp npmrc:', err) + } + } } } +/** + * Creates a temporary npmrc containing the GitHub Packages registry/auth settings. + * + * pnpm 10 reliably consumes these scoped auth settings from an npmrc passed through + * `npm_config_userconfig`; passing the same keys directly as environment variables is + * not portable across build providers. + */ +function createTempNpmrc(authToken) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cowswap-pnpm-')) + const npmrcPath = path.join(dir, '.npmrc') + + try { + fs.writeFileSync( + npmrcPath, + `@cowprotocol:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${authToken}\n`, + { mode: 0o600 }, + ) + } catch (err) { + fs.rmSync(dir, { force: true, recursive: true }) + throw err + } + + return { dir, path: npmrcPath } +} + +/** + * Deletes the temporary npmrc directory if it exists. + */ +function removeTempNpmrc(dir) { + fs.rmSync(dir, { force: true, recursive: true }) +} + /** * Creates a temporary pnpm hook file that rewrites workspace manifests in memory. * diff --git a/tools/scripts/install.test.mjs b/tools/scripts/install.test.mjs new file mode 100644 index 0000000000..97f5bb8204 --- /dev/null +++ b/tools/scripts/install.test.mjs @@ -0,0 +1,124 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { describe, it } from 'node:test' +import vm from 'node:vm' + +describe('install.js', () => { + it('passes GitHub Packages auth to pnpm through a temporary npmrc', () => { + const script = readFileSync(new URL('./install.js', import.meta.url), 'utf8') + const execCalls = [] + const writes = [] + const removals = [] + + const fakeFs = { + existsSync() { + return false + }, + mkdtempSync(prefix) { + assert.equal(prefix, '/tmp/cowswap-pnpm-') + return '/tmp/cowswap-pnpm-test' + }, + readFileSync() { + throw new Error('unexpected fs.readFileSync call') + }, + readdirSync() { + throw new Error('unexpected fs.readdirSync call') + }, + rmSync(path, options) { + removals.push({ path, options }) + }, + writeFileSync(path, content, options) { + writes.push({ path, content, options }) + }, + } + + const sandbox = { + __dirname: '/repo/tools/scripts', + console: { + error() {}, + log() {}, + warn() {}, + }, + process: { + env: { PACKAGE_READ_AUTH_TOKEN: 'secret-token' }, + exit(code) { + throw new Error(`unexpected process.exit(${code})`) + }, + }, + require(id) { + if (id === 'child_process') { + return { + execSync(command, options) { + execCalls.push({ command, options }) + }, + } + } + + if (id === 'fs') return fakeFs + if (id === 'os') { + return { + tmpdir() { + return '/tmp' + }, + } + } + if (id === 'path') return pathModule + if (id === '../../apps/cowswap-frontend/package.json') { + return { + dependencies: { + '@cowprotocol/sdk-bridging': '4.1.2-pr-897-c3d02aa9.0', + }, + } + } + + throw new Error(`unexpected require(${id})`) + }, + } + + vm.runInNewContext( + `(function(require, process, console, __dirname) {\n${script}\n})(require, process, console, __dirname)`, + sandbox, + ) + + assert.equal(execCalls.length, 1) + const [{ command, options }] = execCalls + + assert.equal(command, 'pnpm install --no-frozen-lockfile') + assert.equal(options.cwd, '/repo') + assert.equal(options.env.npm_config_userconfig, '/tmp/cowswap-pnpm-test/.npmrc') + assert.equal(options.env['npm_config_//npm.pkg.github.com/:_authToken'], undefined) + assert.equal(options.env['npm_config_@cowprotocol:registry'], undefined) + + assert.equal(writes.length, 1) + assert.equal(writes[0].path, '/tmp/cowswap-pnpm-test/.npmrc') + assert.equal( + writes[0].content, + '@cowprotocol:registry=https://npm.pkg.github.com\n' + + '//npm.pkg.github.com/:_authToken=secret-token\n', + ) + assert.equal(writes[0].options.mode, 0o600) + + assert.equal(removals.length, 1) + assert.equal(removals[0].path, '/tmp/cowswap-pnpm-test') + assert.equal(removals[0].options.force, true) + assert.equal(removals[0].options.recursive, true) + }) +}) + +const pathModule = { + join(...parts) { + return parts.join('/').replace(/\/+/g, '/') + }, + resolve(...parts) { + const joined = parts.join('/') + const segments = [] + + for (const segment of joined.split('/')) { + if (!segment || segment === '.') continue + if (segment === '..') segments.pop() + else segments.push(segment) + } + + return '/' + segments.join('/') + }, +} From 75f6cf7ce7a41fa23d67a2b44029b352eefea369 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:19:24 +0000 Subject: [PATCH 2/5] chore(i18n): extract i18n strings [automatic] --- apps/cowswap-frontend/src/locales/en-US.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/locales/en-US.po b/apps/cowswap-frontend/src/locales/en-US.po index b0cc98976b..5b7042d8fa 100644 --- a/apps/cowswap-frontend/src/locales/en-US.po +++ b/apps/cowswap-frontend/src/locales/en-US.po @@ -3182,8 +3182,8 @@ msgid "Rewards will be sent on Ethereum to" msgstr "Rewards will be sent on Ethereum to" #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx -msgid "Bridging without swapping is not yet supported. Let us know if you want this feature!" -msgstr "Bridging without swapping is not yet supported. Let us know if you want this feature!" +#~ msgid "Bridging without swapping is not yet supported. Let us know if you want this feature!" +#~ msgstr "Bridging without swapping is not yet supported. Let us know if you want this feature!" #: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx msgid "Rewards end" @@ -7128,8 +7128,8 @@ msgid "Advanced" msgstr "Advanced" #: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx -msgid "Not yet supported" -msgstr "Not yet supported" +#~ msgid "Not yet supported" +#~ msgstr "Not yet supported" #: apps/cowswap-frontend/src/modules/affiliate/pure/AffiliateTraderModal/AffiliateTradeCodeForm.tsx #: apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx From f2f69b0162d6e93147b01872f23be06748a7896c Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Wed, 10 Jun 2026 14:59:20 +0100 Subject: [PATCH 3/5] refactor: remove unnecessary 1 liner fns --- tools/scripts/install.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/tools/scripts/install.js b/tools/scripts/install.js index 66d2bbb60f..7af80a9c89 100644 --- a/tools/scripts/install.js +++ b/tools/scripts/install.js @@ -238,7 +238,7 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { } finally { if (hasRewrites) { try { - removeTempPnpmfile() + fs.rmSync(TEMP_PNPMFILE_PATH, { force: true }) } catch (err) { console.warn('[install.js] Failed to remove temp pnpmfile:', err) } @@ -255,7 +255,7 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) { } if (tempNpmrcDir !== null) { try { - removeTempNpmrc(tempNpmrcDir) + fs.rmSync(tempNpmrcDir, { force: true, recursive: true }) } catch (err) { console.warn('[install.js] Failed to remove temp npmrc:', err) } @@ -288,13 +288,6 @@ function createTempNpmrc(authToken) { return { dir, path: npmrcPath } } -/** - * Deletes the temporary npmrc directory if it exists. - */ -function removeTempNpmrc(dir) { - fs.rmSync(dir, { force: true, recursive: true }) -} - /** * Creates a temporary pnpm hook file that rewrites workspace manifests in memory. * @@ -324,10 +317,3 @@ function createTempPnpmfile(rewriteMap) { fs.writeFileSync(TEMP_PNPMFILE_PATH, pnpmfile) } - -/** - * Deletes the temporary pnpm hook file if it exists. - */ -function removeTempPnpmfile() { - fs.rmSync(TEMP_PNPMFILE_PATH, { force: true }) -} From 52baf6a9043473066be1847c75fdf5984d9c553a Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Wed, 10 Jun 2026 15:04:15 +0100 Subject: [PATCH 4/5] test: updated unit test to cover case when script fails --- tools/scripts/install.test.mjs | 217 ++++++++++++++++++++------------- 1 file changed, 132 insertions(+), 85 deletions(-) diff --git a/tools/scripts/install.test.mjs b/tools/scripts/install.test.mjs index 97f5bb8204..40d6b97f68 100644 --- a/tools/scripts/install.test.mjs +++ b/tools/scripts/install.test.mjs @@ -5,105 +5,152 @@ import vm from 'node:vm' describe('install.js', () => { it('passes GitHub Packages auth to pnpm through a temporary npmrc', () => { - const script = readFileSync(new URL('./install.js', import.meta.url), 'utf8') - const execCalls = [] - const writes = [] - const removals = [] - - const fakeFs = { - existsSync() { - return false - }, - mkdtempSync(prefix) { - assert.equal(prefix, '/tmp/cowswap-pnpm-') - return '/tmp/cowswap-pnpm-test' - }, - readFileSync() { - throw new Error('unexpected fs.readFileSync call') - }, - readdirSync() { - throw new Error('unexpected fs.readdirSync call') - }, - rmSync(path, options) { - removals.push({ path, options }) + const context = createInstallScriptContext() + + context.run() + + assert.equal(context.execCalls.length, 1) + const [{ command, options }] = context.execCalls + + assert.equal(command, 'pnpm install --no-frozen-lockfile') + assert.equal(options.cwd, '/repo') + assert.equal(options.env.npm_config_userconfig, '/tmp/cowswap-pnpm-test/.npmrc') + assert.equal(options.env['npm_config_//npm.pkg.github.com/:_authToken'], undefined) + assert.equal(options.env['npm_config_@cowprotocol:registry'], undefined) + + assertTempNpmrcWrittenAndRemoved(context) + }) + + it('removes the temporary npmrc when pnpm install fails', () => { + const failure = new Error('fail') + const exitError = new Error('process.exit') + let exitCode + const context = createInstallScriptContext({ + execSync() { + throw failure }, - writeFileSync(path, content, options) { - writes.push({ path, content, options }) + processExit(code) { + exitCode = code + throw exitError }, + }) + let caughtError + + try { + context.run() + } catch (err) { + caughtError = err } - const sandbox = { - __dirname: '/repo/tools/scripts', - console: { - error() {}, - log() {}, - warn() {}, - }, - process: { - env: { PACKAGE_READ_AUTH_TOKEN: 'secret-token' }, - exit(code) { - throw new Error(`unexpected process.exit(${code})`) - }, + assert.equal(caughtError, exitError) + assert.equal(exitCode, 1) + assertTempNpmrcWrittenAndRemoved(context) + }) +}) + +function createInstallScriptContext({ execSync = () => {}, processExit = defaultProcessExit } = {}) { + const script = readFileSync(new URL('./install.js', import.meta.url), 'utf8') + const execCalls = [] + const writes = [] + const removals = [] + + const fakeFs = { + existsSync() { + return false + }, + mkdtempSync(prefix) { + assert.equal(prefix, '/tmp/cowswap-pnpm-') + return '/tmp/cowswap-pnpm-test' + }, + readFileSync() { + throw new Error('unexpected fs.readFileSync call') + }, + readdirSync() { + throw new Error('unexpected fs.readdirSync call') + }, + rmSync(path, options) { + removals.push({ path, options }) + }, + writeFileSync(path, content, options) { + writes.push({ path, content, options }) + }, + } + + const sandbox = { + __dirname: '/repo/tools/scripts', + console: { + error() {}, + log() {}, + warn() {}, + }, + process: { + env: { PACKAGE_READ_AUTH_TOKEN: 'secret-token' }, + exit(code) { + processExit(code) }, - require(id) { - if (id === 'child_process') { - return { - execSync(command, options) { - execCalls.push({ command, options }) - }, - } + }, + require(id) { + if (id === 'child_process') { + return { + execSync(command, options) { + execCalls.push({ command, options }) + execSync(command, options) + }, } + } - if (id === 'fs') return fakeFs - if (id === 'os') { - return { - tmpdir() { - return '/tmp' - }, - } + if (id === 'fs') return fakeFs + if (id === 'os') { + return { + tmpdir() { + return '/tmp' + }, } - if (id === 'path') return pathModule - if (id === '../../apps/cowswap-frontend/package.json') { - return { - dependencies: { - '@cowprotocol/sdk-bridging': '4.1.2-pr-897-c3d02aa9.0', - }, - } + } + if (id === 'path') return pathModule + if (id === '../../apps/cowswap-frontend/package.json') { + return { + dependencies: { + '@cowprotocol/sdk-bridging': '4.1.2-pr-897-c3d02aa9.0', + }, } + } - throw new Error(`unexpected require(${id})`) - }, - } + throw new Error(`unexpected require(${id})`) + }, + } - vm.runInNewContext( - `(function(require, process, console, __dirname) {\n${script}\n})(require, process, console, __dirname)`, - sandbox, - ) + return { + execCalls, + removals, + run() { + vm.runInNewContext( + `(function(require, process, console, __dirname) {\n${script}\n})(require, process, console, __dirname)`, + sandbox, + ) + }, + writes, + } +} - assert.equal(execCalls.length, 1) - const [{ command, options }] = execCalls +function defaultProcessExit(code) { + throw new Error(`unexpected process.exit(${code})`) +} - assert.equal(command, 'pnpm install --no-frozen-lockfile') - assert.equal(options.cwd, '/repo') - assert.equal(options.env.npm_config_userconfig, '/tmp/cowswap-pnpm-test/.npmrc') - assert.equal(options.env['npm_config_//npm.pkg.github.com/:_authToken'], undefined) - assert.equal(options.env['npm_config_@cowprotocol:registry'], undefined) +function assertTempNpmrcWrittenAndRemoved({ removals, writes }) { + assert.equal(writes.length, 1) + assert.equal(writes[0].path, '/tmp/cowswap-pnpm-test/.npmrc') + assert.equal( + writes[0].content, + '@cowprotocol:registry=https://npm.pkg.github.com\n' + '//npm.pkg.github.com/:_authToken=secret-token\n', + ) + assert.equal(writes[0].options.mode, 0o600) - assert.equal(writes.length, 1) - assert.equal(writes[0].path, '/tmp/cowswap-pnpm-test/.npmrc') - assert.equal( - writes[0].content, - '@cowprotocol:registry=https://npm.pkg.github.com\n' + - '//npm.pkg.github.com/:_authToken=secret-token\n', - ) - assert.equal(writes[0].options.mode, 0o600) - - assert.equal(removals.length, 1) - assert.equal(removals[0].path, '/tmp/cowswap-pnpm-test') - assert.equal(removals[0].options.force, true) - assert.equal(removals[0].options.recursive, true) - }) -}) + assert.equal(removals.length, 1) + assert.equal(removals[0].path, '/tmp/cowswap-pnpm-test') + assert.equal(removals[0].options.force, true) + assert.equal(removals[0].options.recursive, true) +} const pathModule = { join(...parts) { From 82457b664d0d145d67c163f27368b832f8392ba9 Mon Sep 17 00:00:00 2001 From: Alfetopito Date: Thu, 11 Jun 2026 11:20:15 +0100 Subject: [PATCH 5/5] test: fixed unit tests broken in parent PR --- .../hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap | 1 + .../modules/tradeQuote/services/fetchAndProcessQuote.test.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap index 401f665287..175d30693e 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/__snapshots__/useTradeQuotePolling.test.tsx.snap @@ -78,6 +78,7 @@ exports[`useTradeQuotePolling() When wallet is connected Then should put account "validFor": 1800, }, { + "allowIntermediateEqSellToken": true, "appData": undefined, "getCorrelatedTokens": [Function], "getSlippageSuggestion": undefined, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts index 66d8ae7c15..341db0a378 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.test.ts @@ -171,6 +171,7 @@ describe('fetchAndProcessQuote', () => { quoteRequest: { priceQuality: PriceQuality.FAST, }, + allowIntermediateEqSellToken: true, appData: mockAppData, quoteSigner: undefined, getSlippageSuggestion: undefined, @@ -214,6 +215,7 @@ describe('fetchAndProcessQuote', () => { quoteRequest: { priceQuality: PriceQuality.FAST, }, + allowIntermediateEqSellToken: true, appData: mockAppData, quoteSigner: {}, }), @@ -251,6 +253,7 @@ describe('fetchAndProcessQuote', () => { quoteRequest: { priceQuality: PriceQuality.OPTIMAL, }, + allowIntermediateEqSellToken: true, appData: mockAppData, quoteSigner: undefined, getSlippageSuggestion: undefined, @@ -285,6 +288,7 @@ describe('fetchAndProcessQuote', () => { quoteRequest: { priceQuality: PriceQuality.FAST, }, + allowIntermediateEqSellToken: true, appData: mockAppData, quoteSigner: undefined, getSlippageSuggestion: undefined, @@ -684,6 +688,7 @@ describe('fetchAndProcessQuote', () => { quoteRequest: { priceQuality: PriceQuality.FAST, }, + allowIntermediateEqSellToken: true, appData: undefined, quoteSigner: undefined, getSlippageSuggestion: undefined,