diff --git a/lib/util/exec/buildpack.spec.ts b/lib/util/exec/buildpack.spec.ts
index 050ac92da49eb0daeb938979dbdf992da313fcf8..7f489a182bd042da281fa2a45dc3bac5a7f3ff02 100644
--- a/lib/util/exec/buildpack.spec.ts
+++ b/lib/util/exec/buildpack.spec.ts
@@ -48,13 +48,14 @@ describe('util/exec/buildpack', () => {
 
   describe('resolveConstraint()', () => {
     beforeEach(() => {
-      datasource.getPkgReleases.mockResolvedValueOnce({
+      datasource.getPkgReleases.mockResolvedValue({
         releases: [
           { version: '1.0.0' },
           { version: '1.1.0' },
           { version: '1.3.0' },
           { version: '2.0.14' },
           { version: '2.1.0' },
+          { version: '2.2.0-pre.0' },
         ],
       });
     });
@@ -65,10 +66,36 @@ describe('util/exec/buildpack', () => {
       ).toBe('1.1.0');
     });
 
-    it('returns from latest', async () => {
+    it('returns highest stable', async () => {
       expect(await resolveConstraint({ toolName: 'composer' })).toBe('2.1.0');
     });
 
+    it('returns highest unstable', async () => {
+      datasource.getPkgReleases.mockResolvedValue({
+        releases: [{ version: '2.0.14-b.1' }, { version: '2.1.0-a.1' }],
+      });
+      expect(await resolveConstraint({ toolName: 'composer' })).toBe(
+        '2.1.0-a.1'
+      );
+    });
+
+    it('respects latest', async () => {
+      datasource.getPkgReleases.mockResolvedValue({
+        tags: {
+          latest: '2.0.14',
+        },
+        releases: [
+          { version: '1.0.0' },
+          { version: '1.1.0' },
+          { version: '1.3.0' },
+          { version: '2.0.14' },
+          { version: '2.1.0' },
+          { version: '2.2.0-pre.0' },
+        ],
+      });
+      expect(await resolveConstraint({ toolName: 'composer' })).toBe('2.0.14');
+    });
+
     it('throws for unknown tools', async () => {
       datasource.getPkgReleases.mockReset();
       datasource.getPkgReleases.mockResolvedValueOnce({
diff --git a/lib/util/exec/buildpack.ts b/lib/util/exec/buildpack.ts
index 2d79ff792cc09d373df22693e106c06cb5f9dbeb..ab9202a7fd777e2738e8fe440b3923db8be9b43f 100644
--- a/lib/util/exec/buildpack.ts
+++ b/lib/util/exec/buildpack.ts
@@ -1,3 +1,4 @@
+import is from '@sindresorhus/is';
 import { quote } from 'shlex';
 import { GlobalConfig } from '../../config/global';
 import { logger } from '../../logger';
@@ -100,6 +101,22 @@ export function isDynamicInstall(
   );
 }
 
+function isStable(
+  version: string,
+  versioning: allVersioning.VersioningApi,
+  latest?: string
+): boolean {
+  if (!versioning.isStable(version)) {
+    return false;
+  }
+  if (is.string(latest)) {
+    if (versioning.isGreaterThan(version, latest)) {
+      return false;
+    }
+  }
+  return true;
+}
+
 export async function resolveConstraint(
   toolConstraint: ToolConstraint
 ): Promise<string> {
@@ -124,27 +141,51 @@ export async function resolveConstraint(
 
   const pkgReleases = await getPkgReleases(toolConfig);
   const releases = pkgReleases?.releases ?? [];
-  const versions = releases.map((r) => r.version);
-  const resolvedVersion = versions
-    .filter((v) =>
-      constraint ? versioning.matches(v, constraint) : versioning.isStable(v)
-    )
-    .pop();
 
-  if (resolvedVersion) {
-    logger.debug({ toolName, constraint, resolvedVersion }, 'Resolved version');
-    return resolvedVersion;
+  if (!releases?.length) {
+    throw new Error('No tool releases found.');
   }
 
-  const latestVersion = versions.filter((v) => versioning.isStable(v)).pop();
-  if (!latestVersion) {
-    throw new Error('No tool releases found.');
+  const matchingReleases = releases.filter(
+    (r) => !constraint || versioning.matches(r.version, constraint)
+  );
+
+  const stableMatchingVersion = matchingReleases
+    .filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
+    .pop()?.version;
+  if (stableMatchingVersion) {
+    logger.debug(
+      { toolName, constraint, resolvedVersion: stableMatchingVersion },
+      'Resolved stable matching version'
+    );
+    return stableMatchingVersion;
+  }
+
+  const unstableMatchingVersion = matchingReleases.pop()?.version;
+  if (unstableMatchingVersion) {
+    logger.debug(
+      { toolName, constraint, resolvedVersion: unstableMatchingVersion },
+      'Resolved unstable matching version'
+    );
+    return unstableMatchingVersion;
   }
+
+  const stableVersion = releases
+    .filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
+    .pop()?.version;
+  if (stableVersion) {
+    logger.warn(
+      { toolName, constraint, stableVersion },
+      'No matching tool versions found for constraint - using latest stable version'
+    );
+  }
+
+  const highestVersion = releases.pop()!.version;
   logger.warn(
-    { toolName, constraint, latestVersion },
-    'No matching tool versions found for constraint - using latest version'
+    { toolName, constraint, highestVersion },
+    'No matching or stable tool versions found - using an unstable version'
   );
-  return latestVersion;
+  return highestVersion;
 }
 
 export async function generateInstallCommands(