feat: Allow caching global environment(s) (#226)

This commit is contained in:
Olivier Lacroix
2025-10-20 00:37:21 +11:00
committed by GitHub
parent 194d461b21
commit 28eb668aaf
8 changed files with 391 additions and 172 deletions
+29 -26
View File
@@ -23,7 +23,7 @@ GitHub Action to set up the [pixi](https://github.com/prefix-dev/pixi) package m
## Usage
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
pixi-version: v0.49.0
@@ -35,7 +35,7 @@ GitHub Action to set up the [pixi](https://github.com/prefix-dev/pixi) package m
> [!WARNING]
> Since pixi is not yet stable, the API of this action may change between minor versions.
> Please pin the versions of this action to a specific version (i.e., `prefix-dev/setup-pixi@v0.9.1`) to avoid breaking changes.
> Please pin the versions of this action to a specific version (i.e., `prefix-dev/setup-pixi@v0.9.2`) to avoid breaking changes.
> You can automatically update the version of this action by using [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot).
>
> Put the following in your `.github/dependabot.yml` file to enable Dependabot for your GitHub Actions:
@@ -59,14 +59,17 @@ To see all available input arguments, see the [`action.yml`](action.yml) file.
### Caching
The action supports caching of the pixi environment.
By default, caching is enabled if a `pixi.lock` file is present.
The action supports caching of the project and global pixi environments.
By default, project environment caching is enabled if a `pixi.lock` file is present.
It will then use the `pixi.lock` file to generate a hash of the environment and cache it.
If the cache is hit, the action will skip the installation and use the cached environment.
You can specify the behavior by setting the `cache` input argument.
If you need to customize your cache-key, you can use the `cache-key` input argument.
This will be the prefix of the cache key. The full cache key will be `<cache-key><conda-arch>-<hash>`.
Global environment caching is disabled by default and can be enabled by setting the `global-cache` input to `true`.
As there is no lockfile for global environments, the cache will expire at the end of every month to ensure it does not go stale.
If you need to customize your cache-key, you can use the `cache-key` and `global-cache-key` input arguments.
These will be the prefixes of the cache keys. The full cache keys will be `<cache-key><conda-arch>-<hash>` and `<global-cache-key><conda-arch>-<YYYY-MM>-<hash>` respectively.
#### Only save caches on `main`
@@ -74,7 +77,7 @@ In order to not exceed the [10 GB cache size limit](https://docs.github.com/en/a
This can be done by setting the `cache-write` argument.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
cache: true
cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
@@ -119,7 +122,7 @@ test:
environment: [py311, py312]
steps:
- uses: actions/checkout@v4
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
environments: ${{ matrix.environment }}
```
@@ -129,7 +132,7 @@ test:
The following example will install both the `py311` and the `py312` environment on the runner.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
# separated by spaces
environments: >-
@@ -149,10 +152,10 @@ You can specify `pixi global install` commands by setting the `global-environmen
This will create one environment per line, and install them.
This is useful in particular to install executables that are needed for `pixi install` to work properly.
For instance, the `keyring`, or `gcloud` executables. The following example shows how to install both in separate global environments.
Note that global environments are not cached.
By default, global environments are not cached. You can enable caching by setting the `global-cache` input to `true`.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
global-environments: |
google-cloud-sdk
@@ -185,7 +188,7 @@ Specify the token using the `auth-token` input argument.
This form of authentication (bearer token in the request headers) is mainly used at [prefix.dev](https://prefix.dev).
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
auth-host: prefix.dev
auth-token: ${{ secrets.PREFIX_DEV_TOKEN }}
@@ -197,7 +200,7 @@ Specify the username and password using the `auth-username` and `auth-password`
This form of authentication (HTTP Basic Auth) is used in some enterprise environments with [artifactory](https://jfrog.com/artifactory) for example.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
auth-host: custom-artifactory.com
auth-username: ${{ secrets.PIXI_USERNAME }}
@@ -210,7 +213,7 @@ Specify the conda-token using the `auth-conda-token` input argument.
This form of authentication (token is encoded in URL: `https://my-quetz-instance.com/t/<token>/get/custom-channel`) is used at [anaconda.org](https://anaconda.org) or with [quetz instances](https://github.com/mamba-org/quetz).
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
auth-host: anaconda.org # or my-quetz-instance.com
auth-conda-token: ${{ secrets.CONDA_TOKEN }}
@@ -222,7 +225,7 @@ Specify the S3 key pair using the `auth-access-key-id` and `auth-secret-access-k
You can also specify the session token using the `auth-session-token` input argument.
```yaml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
auth-host: s3://my-s3-bucket
auth-s3-access-key-id: ${{ secrets.ACCESS_KEY_ID }}
@@ -238,7 +241,7 @@ See the [pixi documentation](https://pixi.sh/latest/advanced/s3) for more inform
You can specify whether to use keyring to look up credentials for PyPI.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
pypi-keyring-provider: subprocess # one of 'subprocess', 'disabled'
```
@@ -306,7 +309,7 @@ To this end, `setup-pixi` adds all environment variables set when executing `pix
As a result, all installed binaries can be accessed without having to call `pixi run`.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
activate-environment: true
```
@@ -314,7 +317,7 @@ As a result, all installed binaries can be accessed without having to call `pixi
If you are installing multiple environments, you will need to specify the name of the environment that you want to be activated.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
environments: >-
py311
@@ -331,7 +334,7 @@ You can specify whether `setup-pixi` should run `pixi install --frozen` or `pixi
See the [official documentation](https://prefix.dev/docs/pixi/cli#install) for more information about the `--frozen` and `--locked` flags.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
locked: true
# or
@@ -350,7 +353,7 @@ The first one is the debug logging of the action itself.
This can be enabled by running the action with the `RUNNER_DEBUG` environment variable set to `true`.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
env:
RUNNER_DEBUG: true
```
@@ -368,7 +371,7 @@ The second type is the debug logging of the pixi executable.
This can be specified by setting the `log-level` input.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
# one of `q`, `default`, `v`, `vv`, or `vvv`.
log-level: vvv
@@ -394,7 +397,7 @@ If nothing is specified, `post-cleanup` will default to `true`.
On self-hosted runners, you also might want to alter the default pixi install location to a temporary location. You can use `pixi-bin-path: ${{ runner.temp }}/bin/pixi` to do this.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
post-cleanup: true
# ${{ runner.temp }}\Scripts\pixi.exe on Windows
@@ -410,7 +413,7 @@ You can also use a preinstalled local version of pixi on the runner by not setti
This can be overwritten by setting the `manifest-path` input argument.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
manifest-path: pyproject.toml
```
@@ -420,7 +423,7 @@ This can be overwritten by setting the `manifest-path` input argument.
If you only want to install pixi and not install the current project, you can use the `run-install` option.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
run-install: false
```
@@ -431,7 +434,7 @@ You can also download pixi from a custom URL by setting the `pixi-url` input arg
Optionally, you can combine this with the `pixi-url-headers` input argument to supply additional headers for the download request, such as a bearer token.
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
pixi-url: https://pixi-mirror.example.com/releases/download/v0.48.0/pixi-x86_64-unknown-linux-musl
pixi-url-headers: '{"Authorization": "Bearer ${{ secrets.PIXI_MIRROR_BEARER_TOKEN }}"}'
@@ -447,7 +450,7 @@ It will be rendered with the following variables:
By default, `pixi-url` is equivalent to the following template:
```yml
- uses: prefix-dev/setup-pixi@v0.9.1
- uses: prefix-dev/setup-pixi@v0.9.2
with:
pixi-url: |
{{#if latest~}}
+9 -1
View File
@@ -37,13 +37,21 @@ inputs:
description: Whether to use `pixi install --frozen`. Defaults to `false`.
cache:
description: Whether to cache the pixi environment. Defaults to `true`. Only works if `pixi.lock` is present.
global-cache:
description: |
Whether to cache the global environment(s). Defaults to `false`. As there is no lockfile for global environment,
the global cache will expire at the end of every month, to ensure it does not go stale.
cache-key:
description: |
Cache key prefix to use for caching the pixi environment.
Defaults to `pixi-`. The full cache key is `<cache-key><conda-arch>-<sha-256-of-pixi-lock>`.
global-cache-key:
description: |
Cache key prefix to use for caching the global environments.
Defaults to `pixi-global-`. The full cache key is `<global-cache-key><conda-arch>-<sha-256-of-global-environments>`.
cache-write:
description: |
Whether to write to the cache or only read from it. Defaults to `true`.
Whether to write to the cache or only read from it. Defaults to `true`. Applies to both project and global caches as the case may be.
pixi-bin-path:
description: |
Path to the pixi binary to use. Defaults to `~/.pixi/bin/pixi`.
Generated Vendored
+146 -67
View File
@@ -21662,11 +21662,11 @@ var require_tool_cache = __commonJS({
let toolPath = "";
if (versionSpec) {
versionSpec = semver.clean(versionSpec) || "";
const cachePath2 = path4.join(_getCacheDirectory(), toolName, versionSpec, arch);
core6.debug(`checking cache: ${cachePath2}`);
if (fs3.existsSync(cachePath2) && fs3.existsSync(`${cachePath2}.complete`)) {
const cachePath = path4.join(_getCacheDirectory(), toolName, versionSpec, arch);
core6.debug(`checking cache: ${cachePath}`);
if (fs3.existsSync(cachePath) && fs3.existsSync(`${cachePath}.complete`)) {
core6.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`);
toolPath = cachePath2;
toolPath = cachePath;
} else {
core6.debug("not found");
}
@@ -70262,7 +70262,7 @@ Other caches with similar key:`);
}));
});
}
function saveCache3(cacheId, archivePath, signedUploadURL, options2) {
function saveCache2(cacheId, archivePath, signedUploadURL, options2) {
return __awaiter7(this, void 0, void 0, function* () {
const uploadOptions = (0, options_1.getUploadOptions)(options2);
if (uploadOptions.useAzureSdk) {
@@ -70285,7 +70285,7 @@ Other caches with similar key:`);
}
});
}
exports2.saveCache = saveCache3;
exports2.saveCache = saveCache2;
}
});
@@ -75622,7 +75622,7 @@ var require_cache4 = __commonJS({
return void 0;
});
}
function saveCache3(paths, key, options2, enableCrossOsArchive = false) {
function saveCache2(paths, key, options2, enableCrossOsArchive = false) {
return __awaiter7(this, void 0, void 0, function* () {
const cacheServiceVersion = (0, config_1.getCacheServiceVersion)();
core6.debug(`Cache service version: ${cacheServiceVersion}`);
@@ -75637,7 +75637,7 @@ var require_cache4 = __commonJS({
}
});
}
exports2.saveCache = saveCache3;
exports2.saveCache = saveCache2;
function saveCacheV1(paths, key, options2, enableCrossOsArchive = false) {
var _a, _b, _c, _d, _e;
return __awaiter7(this, void 0, void 0, function* () {
@@ -79815,11 +79815,17 @@ var validateInputs = (inputs) => {
throw new Error("You need to specify pixi-url when using pixi-url-headers");
}
if (inputs.cacheKey !== void 0 && inputs.cache === false) {
throw new Error("Cannot specify cache key without caching");
throw new Error("Cannot specify project cache key without project caching");
}
if (inputs.globalCacheKey !== void 0 && inputs.globalCache === false) {
throw new Error("Cannot specify global cache key without global caching");
}
if (inputs.runInstall === false && inputs.cache === true) {
throw new Error("Cannot cache without running install");
}
if (inputs.globalCache === true && (!inputs.globalEnvironments || inputs.globalEnvironments.length === 0)) {
throw new Error("Cannot use global-cache without specifying global-environments");
}
if (inputs.runInstall === false && inputs.frozen === true) {
throw new Error("Cannot use `frozen: true` when not running install");
}
@@ -79940,7 +79946,14 @@ var inferOptions = (inputs) => {
} else if (inputs.activateEnvironment && inputs.activateEnvironment !== "false") {
activatedEnvironment = inputs.activateEnvironment;
}
const cache2 = inputs.cacheKey ? { cacheKeyPrefix: inputs.cacheKey, cacheWrite: inputs.cacheWrite ?? true } : inputs.cache === true || lockFileAvailable && inputs.cache !== false ? { cacheKeyPrefix: "pixi-", cacheWrite: inputs.cacheWrite ?? true } : void 0;
const cache2 = inputs.cache === true || lockFileAvailable && inputs.cache !== false ? {
cacheKeyPrefix: inputs.cacheKey ?? "pixi-",
cacheWrite: inputs.cacheWrite ?? true
} : void 0;
const globalCache = inputs.globalCache === true && inputs.globalEnvironments && inputs.globalEnvironments.length > 0 ? {
cacheKeyPrefix: inputs.globalCacheKey ?? "pixi-global-",
cacheWrite: inputs.cacheWrite ?? true
} : void 0;
const frozen = inputs.frozen ?? false;
const locked = inputs.locked ?? (lockFileAvailable && !frozen);
const auth = !inputs.authHost ? void 0 : inputs.authToken ? {
@@ -79975,6 +79988,7 @@ var inferOptions = (inputs) => {
frozen,
locked,
cache: cache2,
globalCache,
pixiBinPath,
auth,
postCleanup
@@ -80003,7 +80017,9 @@ var getOptions = () => {
locked: parseOrUndefinedJSON("locked", boolean2()),
frozen: parseOrUndefinedJSON("frozen", boolean2()),
cache: parseOrUndefinedJSON("cache", boolean2()),
globalCache: parseOrUndefinedJSON("global-cache", boolean2()),
cacheKey: parseOrUndefined("cache-key", string2()),
globalCacheKey: parseOrUndefined("global-cache-key", string2()),
cacheWrite: parseOrUndefinedJSON("cache-write", boolean2()),
pixiBinPath: parseOrUndefined("pixi-bin-path", string2()),
authHost: parseOrUndefined("auth-host", string2()),
@@ -80048,68 +80064,129 @@ var import_promises = __toESM(require("fs/promises"));
var import_path2 = __toESM(require("path"));
var core3 = __toESM(require_core());
var cache = __toESM(require_cache4());
var generateCacheKey = async (cacheKeyPrefix) => Promise.all([import_promises.default.readFile(options.pixiLockFile), import_promises.default.readFile(options.pixiBinPath)]).then(([lockfileContent, pixiBinary]) => {
const lockfileSha = sha256(lockfileContent);
core3.debug(`lockfileSha: ${lockfileSha}`);
const pixiSha = sha256(pixiBinary);
core3.debug(`pixiSha: ${pixiSha}`);
const lockfilePathSha = sha256(options.pixiLockFile);
core3.debug(`lockfilePathSha: ${lockfilePathSha}`);
const environments = sha256(options.environments?.join(" ") ?? "");
core3.debug(`environments: ${environments}`);
const cwdSha = sha256(process.cwd());
core3.debug(`cwdSha: ${cwdSha}`);
const sha = sha256(lockfileSha + environments + pixiSha + lockfilePathSha + cwdSha);
core3.debug(`sha: ${sha}`);
return `${cacheKeyPrefix}${getCondaArch()}-${sha}`;
}).catch((err) => {
throw new Error(`Failed to generate cache key: ${err}`);
});
var cachePath = import_path2.default.join(import_path2.default.dirname(options.pixiLockFile), ".pixi");
var cacheHit = false;
var tryRestoreCache = () => {
var getYearMonth = () => {
return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA", { year: "numeric", month: "2-digit" });
};
var pixiSha;
var getPixiSha = async () => {
if (pixiSha) {
return pixiSha;
}
const pixiBinary = await import_promises.default.readFile(options.pixiBinPath);
pixiSha = sha256(pixiBinary);
return pixiSha;
};
var generateProjectCacheKey = async (cacheKeyPrefix) => {
try {
const [lockfileContent, pixiSha2] = await Promise.all([import_promises.default.readFile(options.pixiLockFile), getPixiSha()]);
const lockfileSha = sha256(lockfileContent);
core3.debug(`lockfileSha: ${lockfileSha}`);
core3.debug(`pixiSha: ${pixiSha2}`);
const lockfilePathSha = sha256(options.pixiLockFile);
core3.debug(`lockfilePathSha: ${lockfilePathSha}`);
const environments = sha256(options.environments?.join(" ") ?? "");
core3.debug(`environments: ${environments}`);
const cwdSha = sha256(process.cwd());
core3.debug(`cwdSha: ${cwdSha}`);
const sha = sha256(lockfileSha + environments + pixiSha2 + lockfilePathSha + cwdSha);
core3.debug(`sha: ${sha}`);
return `${cacheKeyPrefix}${getCondaArch()}-${sha}`;
} catch (err) {
throw new Error(`Failed to generate cache key: ${err}`);
}
};
var generateGlobalCacheKey = async (cacheKeyPrefix) => {
try {
const pixiSha2 = await getPixiSha();
core3.debug(`pixiSha: ${pixiSha2}`);
const globalEnvironments = sha256(options.globalEnvironments?.join(" ") ?? "");
core3.debug(`globalEnvironments: ${globalEnvironments}`);
const sha = sha256(globalEnvironments + pixiSha2 + getGlobalCachePath());
core3.debug(`sha: ${sha}`);
return `${cacheKeyPrefix}${getCondaArch()}-${getYearMonth()}-${sha}`;
} catch (err) {
throw new Error(`Failed to generate cache key: ${err}`);
}
};
var projectCachePath = import_path2.default.join(import_path2.default.dirname(options.pixiLockFile), ".pixi");
var getGlobalCachePath = () => {
const pixiHome = process.env.PIXI_HOME;
if (pixiHome) {
return import_path2.default.join(pixiHome, "envs");
}
const home = process.env.HOME;
if (home) {
return import_path2.default.join(home, ".pixi", "envs");
}
throw new Error("Neither PIXI_HOME nor HOME environment variables are set.");
};
var projectCacheHit = false;
var globalCacheHit = false;
async function _tryRestoreCache(type, keyPrefix, generateKey, cachePath, onHit) {
return core3.group(`Restoring ${type} cache`, async () => {
const cacheKey = await generateKey(keyPrefix);
core3.debug(`Cache key: ${cacheKey}`);
core3.debug(`Cache path: ${cachePath}`);
const key = await cache.restoreCache([cachePath], cacheKey, void 0, void 0, false);
if (key) {
core3.info(`Restored cache with key \`${key}\``);
onHit();
} else {
core3.info(`Cache miss`);
}
return key;
});
}
async function _saveCache(type, wasHit, keyPrefix, generateKey, cachePath) {
if (wasHit) {
core3.debug(`Skipping ${type} cache save because cache was restored.`);
return;
}
await core3.group(`Saving ${type.toLowerCase()} cache`, async () => {
const cacheKey = await generateKey(keyPrefix);
try {
const cacheId = await cache.saveCache([cachePath], cacheKey, void 0, false);
core3.info(`Saved cache with ID "${cacheId.toString()}"`);
} catch (err) {
core3.error(`Error saving ${type} cache: ${err}`);
}
});
}
var tryRestoreProjectCache = async () => {
const cache_ = options.cache;
if (!cache_) {
core3.debug("Skipping pixi cache restore.");
return Promise.resolve(void 0);
core3.debug("Skipping project cache restore.");
return void 0;
}
return core3.group(
"Restoring pixi cache",
() => generateCacheKey(cache_.cacheKeyPrefix).then((cacheKey) => {
core3.debug(`Cache key: ${cacheKey}`);
core3.debug(`Cache path: ${cachePath}`);
return cache.restoreCache([cachePath], cacheKey, void 0, void 0, false).then((key) => {
if (key) {
core3.info(`Restored cache with key \`${key}\``);
cacheHit = true;
} else {
core3.info(`Cache miss`);
}
return key;
});
})
);
return _tryRestoreCache("project", cache_.cacheKeyPrefix, generateProjectCacheKey, projectCachePath, () => {
projectCacheHit = true;
});
};
var saveCache2 = () => {
var tryRestoreGlobalCache = async () => {
const cache_ = options.globalCache;
if (!cache_ || !options.globalEnvironments || options.globalEnvironments.length === 0) {
core3.debug("Skipping global cache restore.");
return void 0;
}
return _tryRestoreCache("global", cache_.cacheKeyPrefix, generateGlobalCacheKey, getGlobalCachePath(), () => {
globalCacheHit = true;
});
};
var saveProjectCache = async () => {
const cache_ = options.cache;
if (!cache_?.cacheWrite) {
core3.debug("Skipping pixi cache save.");
return Promise.resolve(void 0);
if (!cache_?.cacheWrite || !options.runInstall) {
core3.debug("Skipping project cache save.");
return;
}
if (cacheHit) {
core3.debug("Skipping pixi cache save because cache was restored.");
return Promise.resolve(void 0);
await _saveCache("project", projectCacheHit, cache_.cacheKeyPrefix, generateProjectCacheKey, projectCachePath);
};
var saveGlobalCache = async () => {
const cache_ = options.globalCache;
if (!cache_?.cacheWrite || !options.globalEnvironments || options.globalEnvironments.length === 0) {
core3.debug("Skipping global cache save.");
return;
}
return core3.group(
"Saving pixi cache",
() => generateCacheKey(cache_.cacheKeyPrefix).then(
(cacheKey) => cache.saveCache([cachePath], cacheKey, void 0, false).then((cacheId) => {
core3.info(`Saved cache with ID "${cacheId.toString()}"`);
}).catch((err) => {
core3.error(`Error saving cache: ${err}`);
})
)
);
await _saveCache("global", globalCacheHit, cache_.cacheKeyPrefix, generateGlobalCacheKey, getGlobalCachePath());
};
// src/activate.ts
@@ -80211,18 +80288,20 @@ var pixiGlobalInstall = async () => {
core5.debug("Skipping pixi global install.");
return;
}
await tryRestoreGlobalCache();
core5.debug("Installing global environments");
for (const env of globalEnvironments) {
const command = `global install ${env}`;
await core5.group(`pixi ${command}`, () => execute(pixiCmd(command, false)));
}
await saveGlobalCache();
};
var pixiInstall = async () => {
if (!options.runInstall) {
core5.debug("Skipping pixi install.");
return;
}
await tryRestoreCache();
await tryRestoreProjectCache();
const environments = options.environments ?? [void 0];
for (const environment of environments) {
core5.debug(`Installing environment ${environment ?? "default"}`);
@@ -80241,7 +80320,7 @@ var pixiInstall = async () => {
}
await core5.group(`pixi ${command}`, () => execute(pixiCmd(command)));
}
await saveCache2();
await saveProjectCache();
};
var generateList = async () => {
if (!options.runInstall) {
Generated Vendored
+18 -2
View File
@@ -29903,11 +29903,17 @@ var validateInputs = (inputs) => {
throw new Error("You need to specify pixi-url when using pixi-url-headers");
}
if (inputs.cacheKey !== void 0 && inputs.cache === false) {
throw new Error("Cannot specify cache key without caching");
throw new Error("Cannot specify project cache key without project caching");
}
if (inputs.globalCacheKey !== void 0 && inputs.globalCache === false) {
throw new Error("Cannot specify global cache key without global caching");
}
if (inputs.runInstall === false && inputs.cache === true) {
throw new Error("Cannot cache without running install");
}
if (inputs.globalCache === true && (!inputs.globalEnvironments || inputs.globalEnvironments.length === 0)) {
throw new Error("Cannot use global-cache without specifying global-environments");
}
if (inputs.runInstall === false && inputs.frozen === true) {
throw new Error("Cannot use `frozen: true` when not running install");
}
@@ -30028,7 +30034,14 @@ var inferOptions = (inputs) => {
} else if (inputs.activateEnvironment && inputs.activateEnvironment !== "false") {
activatedEnvironment = inputs.activateEnvironment;
}
const cache = inputs.cacheKey ? { cacheKeyPrefix: inputs.cacheKey, cacheWrite: inputs.cacheWrite ?? true } : inputs.cache === true || lockFileAvailable && inputs.cache !== false ? { cacheKeyPrefix: "pixi-", cacheWrite: inputs.cacheWrite ?? true } : void 0;
const cache = inputs.cache === true || lockFileAvailable && inputs.cache !== false ? {
cacheKeyPrefix: inputs.cacheKey ?? "pixi-",
cacheWrite: inputs.cacheWrite ?? true
} : void 0;
const globalCache = inputs.globalCache === true && inputs.globalEnvironments && inputs.globalEnvironments.length > 0 ? {
cacheKeyPrefix: inputs.globalCacheKey ?? "pixi-global-",
cacheWrite: inputs.cacheWrite ?? true
} : void 0;
const frozen = inputs.frozen ?? false;
const locked = inputs.locked ?? (lockFileAvailable && !frozen);
const auth = !inputs.authHost ? void 0 : inputs.authToken ? {
@@ -30063,6 +30076,7 @@ var inferOptions = (inputs) => {
frozen,
locked,
cache,
globalCache,
pixiBinPath,
auth,
postCleanup
@@ -30091,7 +30105,9 @@ var getOptions = () => {
locked: parseOrUndefinedJSON("locked", boolean2()),
frozen: parseOrUndefinedJSON("frozen", boolean2()),
cache: parseOrUndefinedJSON("cache", boolean2()),
globalCache: parseOrUndefinedJSON("global-cache", boolean2()),
cacheKey: parseOrUndefined("cache-key", string2()),
globalCacheKey: parseOrUndefined("global-cache-key", string2()),
cacheWrite: parseOrUndefinedJSON("cache-write", boolean2()),
pixiBinPath: parseOrUndefined("pixi-bin-path", string2()),
authHost: parseOrUndefined("auth-host", string2()),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "setup-pixi",
"version": "0.9.1",
"version": "0.9.2",
"private": true,
"description": "Action to set up the pixi package manager.",
"scripts": {
+149 -67
View File
@@ -5,79 +5,161 @@ import * as cache from '@actions/cache'
import { options } from './options'
import { getCondaArch, sha256 } from './util'
export const generateCacheKey = async (cacheKeyPrefix: string) =>
Promise.all([fs.readFile(options.pixiLockFile), fs.readFile(options.pixiBinPath)])
.then(([lockfileContent, pixiBinary]) => {
const lockfileSha = sha256(lockfileContent)
core.debug(`lockfileSha: ${lockfileSha}`)
const pixiSha = sha256(pixiBinary)
core.debug(`pixiSha: ${pixiSha}`)
// the path to the lock file decides where the pixi env is created (../.pixi/env)
// since conda envs are not relocatable, we need to include the path in the cache key
const lockfilePathSha = sha256(options.pixiLockFile)
core.debug(`lockfilePathSha: ${lockfilePathSha}`)
const environments = sha256(options.environments?.join(' ') ?? '')
core.debug(`environments: ${environments}`)
// since the lockfile path is not necessarily absolute, we need to include the cwd in the cache key
const cwdSha = sha256(process.cwd())
core.debug(`cwdSha: ${cwdSha}`)
const sha = sha256(lockfileSha + environments + pixiSha + lockfilePathSha + cwdSha)
core.debug(`sha: ${sha}`)
return `${cacheKeyPrefix}${getCondaArch()}-${sha}`
})
.catch((err: unknown) => {
const getYearMonth = () => {
// this gets us the current month formatted as YYYY-MM
return new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit' })
}
let pixiSha: string | undefined
const getPixiSha = async () => {
if (pixiSha) {
return pixiSha
}
const pixiBinary = await fs.readFile(options.pixiBinPath)
pixiSha = sha256(pixiBinary)
return pixiSha
}
export const generateProjectCacheKey = async (cacheKeyPrefix: string) => {
try {
const [lockfileContent, pixiSha] = await Promise.all([fs.readFile(options.pixiLockFile), getPixiSha()])
const lockfileSha = sha256(lockfileContent)
core.debug(`lockfileSha: ${lockfileSha}`)
core.debug(`pixiSha: ${pixiSha}`)
// the path to the lock file decides where the pixi env is created (../.pixi/env)
// since conda envs are not relocatable, we need to include the path in the cache key
const lockfilePathSha = sha256(options.pixiLockFile)
core.debug(`lockfilePathSha: ${lockfilePathSha}`)
const environments = sha256(options.environments?.join(' ') ?? '')
core.debug(`environments: ${environments}`)
// since the lockfile path is not necessarily absolute, we need to include the cwd in the cache key
const cwdSha = sha256(process.cwd())
core.debug(`cwdSha: ${cwdSha}`)
const sha = sha256(lockfileSha + environments + pixiSha + lockfilePathSha + cwdSha)
core.debug(`sha: ${sha}`)
return `${cacheKeyPrefix}${getCondaArch()}-${sha}`
} catch (err: unknown) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Failed to generate cache key: ${err}`)
}
}
export const generateGlobalCacheKey = async (cacheKeyPrefix: string) => {
try {
const pixiSha = await getPixiSha()
core.debug(`pixiSha: ${pixiSha}`)
const globalEnvironments = sha256(options.globalEnvironments?.join(' ') ?? '')
core.debug(`globalEnvironments: ${globalEnvironments}`)
const sha = sha256(globalEnvironments + pixiSha + getGlobalCachePath())
core.debug(`sha: ${sha}`)
return `${cacheKeyPrefix}${getCondaArch()}-${getYearMonth()}-${sha}`
} catch (err: unknown) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Failed to generate cache key: ${err}`)
}
}
const projectCachePath = path.join(path.dirname(options.pixiLockFile), '.pixi')
const getGlobalCachePath = () => {
const pixiHome = process.env.PIXI_HOME
if (pixiHome) {
return path.join(pixiHome, 'envs')
}
const home = process.env.HOME
if (home) {
return path.join(home, '.pixi', 'envs')
}
throw new Error('Neither PIXI_HOME nor HOME environment variables are set.')
}
let projectCacheHit = false
let globalCacheHit = false
async function _tryRestoreCache(
type: 'project' | 'global',
keyPrefix: string,
generateKey: (prefix: string) => Promise<string>,
cachePath: string,
onHit: () => void
): Promise<string | undefined> {
return core.group(`Restoring ${type} cache`, async () => {
const cacheKey = await generateKey(keyPrefix)
core.debug(`Cache key: ${cacheKey}`)
core.debug(`Cache path: ${cachePath}`)
const key = await cache.restoreCache([cachePath], cacheKey, undefined, undefined, false)
if (key) {
core.info(`Restored cache with key \`${key}\``)
onHit()
} else {
core.info(`Cache miss`)
}
return key
})
}
async function _saveCache(
type: 'project' | 'global',
wasHit: boolean,
keyPrefix: string,
generateKey: (prefix: string) => Promise<string>,
cachePath: string
) {
if (wasHit) {
core.debug(`Skipping ${type} cache save because cache was restored.`)
return
}
await core.group(`Saving ${type.toLowerCase()} cache`, async () => {
const cacheKey = await generateKey(keyPrefix)
try {
const cacheId = await cache.saveCache([cachePath], cacheKey, undefined, false)
core.info(`Saved cache with ID "${cacheId.toString()}"`)
} catch (err: unknown) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Failed to generate cache key: ${err}`)
})
core.error(`Error saving ${type} cache: ${err}`)
}
})
}
const cachePath = path.join(path.dirname(options.pixiLockFile), '.pixi')
let cacheHit = false
export const tryRestoreCache = (): Promise<string | undefined> => {
export const tryRestoreProjectCache = async (): Promise<string | undefined> => {
const cache_ = options.cache
if (!cache_) {
core.debug('Skipping pixi cache restore.')
return Promise.resolve(undefined)
core.debug('Skipping project cache restore.')
return undefined
}
return core.group('Restoring pixi cache', () =>
generateCacheKey(cache_.cacheKeyPrefix).then((cacheKey) => {
core.debug(`Cache key: ${cacheKey}`)
core.debug(`Cache path: ${cachePath}`)
return cache.restoreCache([cachePath], cacheKey, undefined, undefined, false).then((key) => {
if (key) {
core.info(`Restored cache with key \`${key}\``)
cacheHit = true
} else {
core.info(`Cache miss`)
}
return key
})
})
)
return _tryRestoreCache('project', cache_.cacheKeyPrefix, generateProjectCacheKey, projectCachePath, () => {
projectCacheHit = true
})
}
export const saveCache = () => {
const cache_ = options.cache
if (!cache_?.cacheWrite) {
core.debug('Skipping pixi cache save.')
return Promise.resolve(undefined)
export const tryRestoreGlobalCache = async (): Promise<string | undefined> => {
const cache_ = options.globalCache
if (!cache_ || !options.globalEnvironments || options.globalEnvironments.length === 0) {
core.debug('Skipping global cache restore.')
return undefined
}
if (cacheHit) {
core.debug('Skipping pixi cache save because cache was restored.')
return Promise.resolve(undefined)
}
return core.group('Saving pixi cache', () =>
generateCacheKey(cache_.cacheKeyPrefix).then((cacheKey) =>
cache
.saveCache([cachePath], cacheKey, undefined, false)
.then((cacheId) => {
core.info(`Saved cache with ID "${cacheId.toString()}"`)
})
.catch((err: unknown) => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
core.error(`Error saving cache: ${err}`)
})
)
)
return _tryRestoreCache('global', cache_.cacheKeyPrefix, generateGlobalCacheKey, getGlobalCachePath(), () => {
globalCacheHit = true
})
}
export const saveProjectCache = async () => {
const cache_ = options.cache
if (!cache_?.cacheWrite || !options.runInstall) {
core.debug('Skipping project cache save.')
return
}
await _saveCache('project', projectCacheHit, cache_.cacheKeyPrefix, generateProjectCacheKey, projectCachePath)
}
export const saveGlobalCache = async () => {
const cache_ = options.globalCache
if (!cache_?.cacheWrite || !options.globalEnvironments || options.globalEnvironments.length === 0) {
core.debug('Skipping global cache save.')
return
}
await _saveCache('global', globalCacheHit, cache_.cacheKeyPrefix, generateGlobalCacheKey, getGlobalCachePath())
}
+8 -3
View File
@@ -7,7 +7,7 @@ import { downloadTool } from '@actions/tool-cache'
import type { PixiSource } from './options'
import { options } from './options'
import { execute, pixiCmd, renderPixiUrl } from './util'
import { saveCache, tryRestoreCache } from './cache'
import { tryRestoreGlobalCache, tryRestoreProjectCache, saveGlobalCache, saveProjectCache } from './cache'
import { activateEnvironment } from './activate'
const downloadPixi = async (source: PixiSource) => {
@@ -61,11 +61,16 @@ const pixiGlobalInstall = async () => {
core.debug('Skipping pixi global install.')
return
}
await tryRestoreGlobalCache()
core.debug('Installing global environments')
for (const env of globalEnvironments) {
const command = `global install ${env}`
await core.group(`pixi ${command}`, () => execute(pixiCmd(command, false)))
}
await saveGlobalCache()
}
const pixiInstall = async () => {
@@ -74,7 +79,7 @@ const pixiInstall = async () => {
return
}
await tryRestoreCache()
await tryRestoreProjectCache()
const environments = options.environments ?? [undefined]
for (const environment of environments) {
@@ -95,7 +100,7 @@ const pixiInstall = async () => {
await core.group(`pixi ${command}`, () => execute(pixiCmd(command)))
}
await saveCache()
await saveProjectCache()
}
const generateList = async () => {
+31 -5
View File
@@ -21,7 +21,9 @@ type Inputs = Readonly<{
frozen?: boolean
locked?: boolean
cache?: boolean
globalCache?: boolean
cacheKey?: string
globalCacheKey?: string
cacheWrite?: boolean
pixiBinPath?: string
authHost?: string
@@ -68,6 +70,11 @@ interface Cache {
cacheWrite: boolean
}
interface GlobalCache {
cacheKeyPrefix: string
cacheWrite: boolean
}
export type Options = Readonly<{
pixiSource: PixiSource
downloadPixi: boolean
@@ -79,6 +86,7 @@ export type Options = Readonly<{
frozen: boolean
locked: boolean
cache?: Cache
globalCache?: GlobalCache
pixiBinPath: string
auth?: Auth
pypiKeyringProvider?: 'disabled' | 'subprocess'
@@ -170,11 +178,17 @@ const validateInputs = (inputs: Inputs): void => {
throw new Error('You need to specify pixi-url when using pixi-url-headers')
}
if (inputs.cacheKey !== undefined && inputs.cache === false) {
throw new Error('Cannot specify cache key without caching')
throw new Error('Cannot specify project cache key without project caching')
}
if (inputs.globalCacheKey !== undefined && inputs.globalCache === false) {
throw new Error('Cannot specify global cache key without global caching')
}
if (inputs.runInstall === false && inputs.cache === true) {
throw new Error('Cannot cache without running install')
}
if (inputs.globalCache === true && (!inputs.globalEnvironments || inputs.globalEnvironments.length === 0)) {
throw new Error('Cannot use global-cache without specifying global-environments')
}
if (inputs.runInstall === false && inputs.frozen === true) {
throw new Error('Cannot use `frozen: true` when not running install')
}
@@ -309,10 +323,19 @@ const inferOptions = (inputs: Inputs): Options => {
} else if (inputs.activateEnvironment && inputs.activateEnvironment !== 'false') {
activatedEnvironment = inputs.activateEnvironment
}
const cache = inputs.cacheKey
? { cacheKeyPrefix: inputs.cacheKey, cacheWrite: inputs.cacheWrite ?? true }
: inputs.cache === true || (lockFileAvailable && inputs.cache !== false)
? { cacheKeyPrefix: 'pixi-', cacheWrite: inputs.cacheWrite ?? true }
const cache =
inputs.cache === true || (lockFileAvailable && inputs.cache !== false)
? {
cacheKeyPrefix: inputs.cacheKey ?? 'pixi-',
cacheWrite: inputs.cacheWrite ?? true
}
: undefined
const globalCache =
inputs.globalCache === true && inputs.globalEnvironments && inputs.globalEnvironments.length > 0
? {
cacheKeyPrefix: inputs.globalCacheKey ?? 'pixi-global-',
cacheWrite: inputs.cacheWrite ?? true
}
: undefined
const frozen = inputs.frozen ?? false
const locked = inputs.locked ?? (lockFileAvailable && !frozen)
@@ -356,6 +379,7 @@ const inferOptions = (inputs: Inputs): Options => {
frozen,
locked,
cache,
globalCache,
pixiBinPath,
auth,
postCleanup
@@ -392,7 +416,9 @@ const getOptions = () => {
locked: parseOrUndefinedJSON('locked', z.boolean()),
frozen: parseOrUndefinedJSON('frozen', z.boolean()),
cache: parseOrUndefinedJSON('cache', z.boolean()),
globalCache: parseOrUndefinedJSON('global-cache', z.boolean()),
cacheKey: parseOrUndefined('cache-key', z.string()),
globalCacheKey: parseOrUndefined('global-cache-key', z.string()),
cacheWrite: parseOrUndefinedJSON('cache-write', z.boolean()),
pixiBinPath: parseOrUndefined('pixi-bin-path', z.string()),
authHost: parseOrUndefined('auth-host', z.string()),