Skip to content

Commit

Permalink
feat: remove job id from cache key
Browse files Browse the repository at this point in the history
  • Loading branch information
baptiste0928 committed Jan 5, 2025
1 parent bd2e567 commit 409d7a1
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 254 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Removed the job id from the cache key. This allows the same cache to be used
accross multiple jobs if the installation arguments are the same.
- Improved the cache key generation logic.

## [3.2.0] - 2024-12-26

### Changed
Expand Down
286 changes: 135 additions & 151 deletions dist/index.js

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@
"esbuild": "^0.24.2",
"eslint": "^9.17.0",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.2"
},
"prettier": {
"arrowParens": "avoid",
"singleQuote": true
"singleQuote": true,
"plugins": [
"prettier-plugin-organize-imports"
]
}
}
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 9 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import * as cache from '@actions/cache';
import path from 'node:path';

import { Chalk } from 'chalk';
import path from 'node:path';

import {
type ResolvedVersion,
getInstallSettings,
runCargoInstall,
} from './install';
import { type ResolvedVersion, getInstallSettings } from './install';
import { parseInput } from './parse';
import { resolveRegistryVersion } from './resolve/registry';
import { resolveGitCommit } from './resolve/git';
import { resolveRegistryVersion } from './resolve/registry';

const chalk = new Chalk({ level: 3 });

Expand All @@ -35,6 +33,7 @@ async function run(): Promise<void> {
}
core.info(` path: ${install.path}`);
core.info(` key: ${install.cacheKey}`);
core.info(` command: cargo ${install.args.join(' ')}`);

await io.mkdirP(install.path);
const restored = await cache.restoreCache([install.path], install.cacheKey);
Expand All @@ -50,15 +49,15 @@ async function run(): Promise<void> {
core.startGroup(
`No cached version found, installing ${input.crate} using cargo...`,
);
await runCargoInstall(input, version, install);
await exec.exec('cargo', install.args);

try {
await cache.saveCache([install.path], install.cacheKey);
} catch (error) {
if (error instanceof Error) {
core.warning(error.message);
} else {
core.warning('An error occurred while saving the cache.');
core.warning('An unknown error occurred while saving the cache.');
}
}

Expand Down
168 changes: 81 additions & 87 deletions src/install.ts
Original file line number Diff line number Diff line change
@@ -1,156 +1,150 @@
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import path from 'node:path';
import crypto from 'node:crypto';
import path from 'node:path';

import type { ActionInput } from './parse';

// Resolved version information for the crate
export type ResolvedVersion =
| { version: string }
| { repository: string; commit: string };

// Installation settings for the crate (path and cache key)
export interface InstallSettings {
path: string;
args: string[];
cacheKey: string;
}

// Get the installation settings for the crate (path and cache key)
// Get the installation settings for the crate (path, arguments and cache key)
export async function getInstallSettings(
input: ActionInput,
version: ResolvedVersion,
): Promise<InstallSettings> {
const homePath = process.env.HOME ?? process.env.USERPROFILE;

if (homePath === undefined || homePath === '') {
core.setFailed(
'Could not determine home directory (missing HOME and USERPROFILE environement variables)',
);
core.setFailed('Could not determine home directory');
process.exit(1);
}

const installPath = path.join(homePath, '.cargo-install', input.crate);
const cacheKey = await getCacheKey(input, version);
const args = getInstallArgs(input, version, installPath);
const cacheKey = await getCacheKey(input, version, args);

return {
path: installPath,
args,
cacheKey,
};
}

// Get the os version of the runner, used for the cache key
async function getOsVersion(): Promise<string | undefined> {
const runnerOs = process.env.RUNNER_OS;
// Generate the arguments that will be passed to `cargo` to install the crate.
function getInstallArgs(
input: ActionInput,
version: ResolvedVersion,
installPath: string,
): string[] {
let args = ['install', input.crate, '--force', '--root', installPath];

if (runnerOs === 'Linux') {
const output = await exec.getExecOutput('cat', ['/etc/os-release'], {
silent: true,
});
const match = output.stdout.match(/VERSION_ID="(.*)"/);
return match?.[1];
if ('version' in version) {
args.push('--version', version.version);
} else {
args.push('--git', version.repository, '--rev', version.commit);
}

if (runnerOs === 'macOS') {
const output = await exec.getExecOutput('sw_vers', ['-productVersion'], {
silent: true,
});
return output.stdout.trim();
if (input.source.type === 'registry' && input.source.registry) {
args.push('--registry', input.source.registry);
}
if (input.source.type === 'registry' && input.source.index) {
args.push('--index', input.source.index);
}

if (runnerOs === 'Windows') {
const major = await exec.getExecOutput(
'pwsh',
['-Command', '[System.Environment]::OSVersion.Version.Major'],
{ silent: true },
);
const minor = await exec.getExecOutput(
'pwsh',
['-Command', '[System.Environment]::OSVersion.Version.Minor'],
{ silent: true },
);
return `${major.stdout.trim()}.${minor.stdout.trim()}`;
if (input.features.length > 0) {
args.push('--features', input.features.join(','));
}

if (input.args.length > 0) {
args = args.concat(input.args);
}

return args;
}

async function getCacheKey(
input: ActionInput,
version: ResolvedVersion,
args: string[],
): Promise<string> {
const runnerOs = process.env.RUNNER_OS;
const runnerArch = process.env.RUNNER_ARCH;
const jobId = process.env.GITHUB_JOB;
const osVersion = await getOsVersion();

if (
runnerOs === undefined ||
runnerArch === undefined ||
jobId === undefined
) {
core.setFailed('Could not determine runner OS, runner arch or job ID');
if (runnerOs === undefined || runnerArch === undefined) {
core.setFailed('Could not determine runner OS or runner arch');
process.exit(1);
}

let hashKey = jobId + runnerOs + runnerArch + (osVersion ?? '');

hashKey += input.source.type;
if (input.source.type === 'registry') {
hashKey += input.source.registry ?? '';
hashKey += input.source.index ?? '';
} else {
hashKey += input.source.repository;
hashKey += input.source.branch ?? '';
hashKey += input.source.tag ?? '';
hashKey += input.source.commit ?? '';
}

for (const feature of input.features) {
hashKey += feature;
}
for (const arg of input.args) {
hashKey += arg;
}
if (input.cacheKey?.length > 0) {
hashKey += input.cacheKey;
}
/**
* Most of the cache key is a hash of the parameters that may affect the build
* output. We take only the first 24 characters to improve readability.
*
* The key is composed of:
* - the runner os information (os name, architecture and version)
* - the arguments passed to cargo install (which contain the exact version
* installed, features enabled, ...)
* - additionally, the cache key provided by the user
*/

const hashKey =
runnerOs +
runnerArch +
(osVersion ?? '') +
args.join(' ') +
(input.cacheKey ?? '');

const hash = crypto
.createHash('sha256')
.update(hashKey)
.digest('hex')
.slice(0, 20);
.slice(0, 24);

// We include the installed crate and version in the cache key to make it
// easier to identify if a manual invalidation is needed.
const versionKey =
'version' in version ? version.version : version.commit.slice(0, 7);

return `cargo-install-${input.crate}-${versionKey}-${hash}`;
}

export async function runCargoInstall(
input: ActionInput,
version: ResolvedVersion,
install: InstallSettings,
): Promise<void> {
let commandArgs = ['install', input.crate, '--force', '--root', install.path];

if ('version' in version) {
commandArgs.push('--version', version.version);
} else {
commandArgs.push('--git', version.repository, '--rev', version.commit);
}
async function getOsVersion(): Promise<string | undefined> {
const runnerOs = process.env.RUNNER_OS;

if (input.source.type === 'registry' && input.source.registry !== undefined) {
commandArgs.push('--registry', input.source.registry);
}
if (input.source.type === 'registry' && input.source.index !== undefined) {
commandArgs.push('--index', input.source.index);
if (runnerOs === 'Linux') {
const output = await exec.getExecOutput('cat', ['/etc/os-release'], {
silent: true,
});
const match = output.stdout.match(/VERSION_ID="(.*)"/);
return match?.[1];
}

if (input.features.length > 0) {
commandArgs.push('--features', input.features.join(','));
if (runnerOs === 'macOS') {
const output = await exec.getExecOutput('sw_vers', ['-productVersion'], {
silent: true,
});
return output.stdout.trim();
}

if (input.args.length > 0) {
commandArgs = commandArgs.concat(input.args);
if (runnerOs === 'Windows') {
const major = await exec.getExecOutput(
'pwsh',
['-Command', '[System.Environment]::OSVersion.Version.Major'],
{ silent: true },
);
const minor = await exec.getExecOutput(
'pwsh',
['-Command', '[System.Environment]::OSVersion.Version.Minor'],
{ silent: true },
);
return `${major.stdout.trim()}.${minor.stdout.trim()}`;
}

await exec.exec('cargo', commandArgs);
}
4 changes: 2 additions & 2 deletions src/resolve/git.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as exec from '@actions/exec';

import type { GitSource } from '../parse';
import type { ResolvedVersion } from '../install';
import type { GitSource } from '../parse';

interface GitRemoteCommits {
head: string;
Expand Down
6 changes: 3 additions & 3 deletions src/resolve/registry.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import * as http from '@actions/http-client';
import * as core from '@actions/core';
import * as http from '@actions/http-client';
import semver from 'semver';

import type { ResolvedVersion } from '../install';
import {
InferOutput,
boolean,
check,
object,
parse,
string,
pipe,
string,
} from 'valibot';
import type { ResolvedVersion } from '../install';
import { RegistrySource } from '../parse';

const CrateVersionSchema = object({
Expand Down

0 comments on commit 409d7a1

Please sign in to comment.