Add RHEL support and include Linux distro in cache keys (#1323)

* Add RHEL support for manifest matching and OS detection

* update dist

* make cache keys distro-aware and key RHEL by major version

* Normalize RHEL OS detection and improve cache key consistency

* Refactor OS info retrieval to use getOSInfo and handle null cases for improved reliability
This commit is contained in:
Priya Gupta
2026-06-19 05:17:28 +05:30
committed by GitHub
parent c8813ba1bc
commit 0cb1a84326
9 changed files with 5612 additions and 52 deletions
@@ -1,5 +1,6 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import {getOSInfo, IS_LINUX} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
export enum State {
@@ -22,6 +23,33 @@ abstract class CacheDistributor {
}>;
protected async handleLoadedCache() {}
/**
* Builds the Linux distro portion of a cache key (e.g. `-26.04-Ubuntu`, `-9-rhel`).
* RHEL is keyed by major version since it ships one ABI-stable artifact per major.
*/
protected async getLinuxInfoKeySegment(): Promise<string> {
if (!IS_LINUX) {
return '';
}
const osInfo = await getOSInfo();
if (!osInfo) {
return '';
}
// lsb_release reports RHEL as "RedHatEnterpriseLinux" while /etc/os-release
// reports it as "rhel"; normalize both to "rhel" so the key is consistent.
const normalizedName = osInfo.osName.toLowerCase();
const isRhel =
normalizedName === 'rhel' || normalizedName.includes('redhat');
const osName = isRhel ? 'rhel' : osInfo.osName;
const osVersion = isRhel
? osInfo.osVersion.split('.')[0]
: osInfo.osVersion;
return `-${osVersion}-${osName}`;
}
public async restoreCache() {
const {primaryKey, restoreKey} = await this.computeKeys();
if (primaryKey.endsWith('-')) {
+4 -12
View File
@@ -7,7 +7,7 @@ import * as path from 'path';
import os from 'os';
import CacheDistributor from './cache-distributor';
import {getLinuxInfo, IS_LINUX, IS_WINDOWS} from '../utils';
import {IS_WINDOWS} from '../utils';
import {CACHE_DEPENDENCY_BACKUP_PATH} from './constants';
class PipCache extends CacheDistributor {
@@ -62,17 +62,9 @@ class PipCache extends CacheDistributor {
const hash =
(await glob.hashFiles(this.cacheDependencyPath)) ||
(await glob.hashFiles(this.cacheDependencyBackupPath));
let primaryKey = '';
let restoreKey = '';
if (IS_LINUX) {
const osInfo = await getLinuxInfo();
primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${osInfo.osVersion}-${osInfo.osName}-python-${this.pythonVersion}-${this.packageManager}-${hash}`;
restoreKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-${osInfo.osVersion}-${osInfo.osName}-python-${this.pythonVersion}-${this.packageManager}`;
} else {
primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-python-${this.pythonVersion}-${this.packageManager}-${hash}`;
restoreKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-python-${this.pythonVersion}-${this.packageManager}`;
}
const osSegment = await this.getLinuxInfoKeySegment();
const primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}${osSegment}-python-${this.pythonVersion}-${this.packageManager}-${hash}`;
const restoreKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}${osSegment}-python-${this.pythonVersion}-${this.packageManager}`;
return {
primaryKey,
+2 -1
View File
@@ -32,7 +32,8 @@ class PipenvCache extends CacheDistributor {
protected async computeKeys() {
const hash = await glob.hashFiles(this.patterns);
const primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-python-${this.pythonVersion}-${this.packageManager}-${hash}`;
const osSegment = await this.getLinuxInfoKeySegment();
const primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}${osSegment}-python-${this.pythonVersion}-${this.packageManager}-${hash}`;
const restoreKey = undefined;
return {
primaryKey,
+2 -1
View File
@@ -46,8 +46,9 @@ class PoetryCache extends CacheDistributor {
protected async computeKeys() {
const hash = await glob.hashFiles(this.patterns);
const osSegment = await this.getLinuxInfoKeySegment();
// "v2" is here to invalidate old caches of this cache distributor, which were created broken:
const primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}-python-${this.pythonVersion}-${this.packageManager}-v2-${hash}`;
const primaryKey = `${this.CACHE_KEY_PREFIX}-${process.env['RUNNER_OS']}-${process.arch}${osSegment}-python-${this.pythonVersion}-${this.packageManager}-v2-${hash}`;
const restoreKey = undefined;
return {
primaryKey,
+85
View File
@@ -3,6 +3,8 @@ import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as httpm from '@actions/http-client';
import * as fs from 'fs';
import * as semver from 'semver';
import {ExecOptions} from '@actions/exec/lib/interfaces';
import {IS_WINDOWS, IS_LINUX, getDownloadFileName} from './utils';
import {IToolRelease} from '@actions/tool-cache';
@@ -14,6 +16,70 @@ const MANIFEST_REPO_NAME = 'python-versions';
const MANIFEST_REPO_BRANCH = 'main';
export const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
interface LinuxOsRelease {
id: string;
versionId: string;
}
function getLinuxOsRelease(): LinuxOsRelease | null {
try {
const content = fs.readFileSync('/etc/os-release', 'utf8');
const lines = content.split('\n');
let id = '';
let versionId = '';
for (const line of lines) {
const parts = line.split('=');
if (parts.length === 2) {
const key = parts[0].trim();
const value = parts[1].trim().replace(/^"/, '').replace(/"$/, '');
if (key === 'ID') id = value;
if (key === 'VERSION_ID') versionId = value;
}
}
if (id && versionId) {
return {id, versionId};
}
return null;
} catch {
return null;
}
}
function findRhelRelease(
semanticVersionSpec: string,
architecture: string,
manifest: tc.IToolRelease[],
osVersion: string
): tc.IToolRelease | undefined {
for (const candidate of manifest) {
const version = candidate.version;
core.debug(`check ${version} satisfies ${semanticVersionSpec}`);
if (!semver.satisfies(version, semanticVersionSpec)) continue;
const file = candidate.files.find(item => {
core.debug(
`${item.arch}===${architecture} && ${item.platform}===rhel && ${item.platform_version}===${osVersion}`
);
const archMatch = item.arch === architecture;
const platformMatch = item.platform === 'rhel';
const versionMatch =
!item.platform_version ||
item.platform_version === osVersion ||
osVersion.startsWith(item.platform_version);
return archMatch && platformMatch && versionMatch;
});
if (file) {
core.debug(`matched ${candidate.version}`);
const result = Object.assign({}, candidate);
result.files = [file];
return result;
}
}
return undefined;
}
export async function findReleaseFromManifest(
semanticVersionSpec: string,
architecture: string,
@@ -23,6 +89,23 @@ export async function findReleaseFromManifest(
manifest = await getManifest();
}
// On RHEL, tc.findFromManifest() won't match because os.platform() returns 'linux'
// but manifest entries use platform 'rhel'. Use custom filtering for RHEL.
if (IS_LINUX) {
const osRelease = getLinuxOsRelease();
if (osRelease && osRelease.id === 'rhel') {
core.debug(
`Detected RHEL ${osRelease.versionId}, using custom manifest filtering`
);
return findRhelRelease(
semanticVersionSpec,
architecture,
manifest,
osRelease.versionId
);
}
}
const foundRelease = await tc.findFromManifest(
semanticVersionSpec,
false,
@@ -32,6 +115,7 @@ export async function findReleaseFromManifest(
return foundRelease;
}
function isIToolRelease(obj: any): obj is IToolRelease {
return (
typeof obj === 'object' &&
@@ -48,6 +132,7 @@ function isIToolRelease(obj: any): obj is IToolRelease {
)
);
}
export async function getManifest(): Promise<tc.IToolRelease[]> {
try {
const repoManifest = await getManifestFromRepo();
+29 -6
View File
@@ -173,15 +173,38 @@ async function getMacOSInfo() {
}
export async function getLinuxInfo() {
const {stdout} = await exec.getExecOutput('lsb_release', ['-i', '-r', '-s'], {
silent: true
});
try {
const {stdout} = await exec.getExecOutput(
'lsb_release',
['-i', '-r', '-s'],
{
silent: true
}
);
const [osName, osVersion] = stdout.trim().split('\n');
const [osName, osVersion] = stdout.trim().split('\n');
core.debug(`OS Name: ${osName}, Version: ${osVersion}`);
return {osName, osVersion};
} catch (err) {
core.debug(
`lsb_release failed (${(err as Error).message}). Falling back to /etc/os-release.`
);
core.debug(`OS Name: ${osName}, Version: ${osVersion}`);
const osReleaseContent = fs.readFileSync('/etc/os-release', 'utf8');
const osInfo: {[key: string]: string} = {};
return {osName: osName, osVersion: osVersion};
osReleaseContent.split('\n').forEach(line => {
const [key, value] = line.split('=');
if (key && value) {
osInfo[key.trim()] = value.trim().replace(/"/g, '');
}
});
const osName = osInfo['ID'] || 'Linux';
const osVersion = osInfo['VERSION_ID'] || '';
core.debug(`OS Name: ${osName}, Version: ${osVersion}`);
return {osName, osVersion};
}
}
export async function getOSInfo() {