From 2ca2bc0ba068fa11cae0972dad9727a0a8e27cd4 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Wed, 7 Oct 2026 16:15:34 +0000 Subject: [PATCH] fix(exec): re-run install-tool when switching back to an earlier tool version (#46823) `install-tool` installs a version and also makes it the active one. The memo skipped `install-tool` for any tool version already installed in the run, so after A -> B -> A the last command ran on B. Track the active version per tool instead and skip only when the requested version is already active. Co-authored-by: Claude Opus 5.5 --- lib/util/exec/containerbase.spec.ts | 21 +++++++++++++++++++++ lib/util/exec/containerbase.ts | 10 ++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/util/exec/containerbase.spec.ts b/lib/util/exec/containerbase.spec.ts index 8f5602a89b..82203115e3 100644 --- a/lib/util/exec/containerbase.spec.ts +++ b/lib/util/exec/containerbase.spec.ts @@ -337,6 +337,27 @@ describe('util/exec/containerbase', () => { generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.2.0']); }); + + it('re-issues the install command when switching back to an earlier version', async () => { + const toolConstraints: ToolConstraint[] = [{ toolName: 'composer' }]; + await expect( + generateInstallCommands(toolConstraints, true), + ).resolves.toEqual(['install-tool composer 2.1.0']); + + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '2.2.0' }], + }); + await expect( + generateInstallCommands(toolConstraints, true), + ).resolves.toEqual(['install-tool composer 2.2.0']); + + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [{ version: '2.1.0' }], + }); + await expect( + generateInstallCommands(toolConstraints, true), + ).resolves.toEqual(['install-tool composer 2.1.0']); + }); }); }); }); diff --git a/lib/util/exec/containerbase.ts b/lib/util/exec/containerbase.ts index 56997113b5..d28c384965 100644 --- a/lib/util/exec/containerbase.ts +++ b/lib/util/exec/containerbase.ts @@ -382,7 +382,9 @@ export async function resolveConstraint( // Docker execs run in a fresh `--rm` container each time, so `memoize` must // stay false there - only the persistent-host (binarySource=install) path -// can safely skip a tool it has already installed this run. +// can safely skip a tool whose requested version is already active. +// `install-tool` also activates the version it installs, so the memo tracks +// the active version per tool, not every installed version. export async function generateInstallCommands( toolConstraints: Opt, memoize = false, @@ -393,11 +395,11 @@ export async function generateInstallCommands( const toolVersion = await resolveConstraint(toolConstraint); const { toolName } = toolConstraint; if (memoize) { - const cacheKey = `containerbase-installed:${toolName}:${toolVersion}`; - if (memCache.get(cacheKey)) { + const cacheKey = `containerbase-active:${toolName}`; + if (memCache.get(cacheKey) === toolVersion) { continue; } - memCache.set(cacheKey, true); + memCache.set(cacheKey, toolVersion); } const installCommand = `install-tool ${toolName} ${quote(toolVersion)}`; installCommands.push(installCommand);