diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4db8a0f..dd1e1ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: run: | set -euo pipefail latest_version="$(jq -r '.version' package.json)" - count_expected=20 + count_expected=21 count_actual="$(grep -c "setup-pixi@v$latest_version" README.md || true)" if [ "$count_actual" -ne "$count_expected" ]; then echo "::error file=README.md::Expected $count_expected mentions of \`setup-pixi@v$latest_version\` in README.md, but found $count_actual." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f75c88..8596760 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,55 @@ jobs: pixi run python --version | grep -q 3.11 pixi run test | grep -q "Hello world" + global-environments: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Move pixi.toml + run: mv test/default/* . + - uses: ./ + with: + global-environments: | + cowpy + - run: | + cowpy hello world + - run: | + set -o pipefail + pixi info + test -f ${{ matrix.os == 'windows-latest' && './.pixi/envs/default/python.exe' || './.pixi/envs/default/bin/python' }} + ${{ matrix.os == 'windows-latest' && './.pixi/envs/default/python.exe' || './.pixi/envs/default/bin/python' }} --version | grep -q 3.11 + shell: bash + - run: | + pixi run python --version | grep -q 3.11 + pixi run test | grep -q "Hello world" + + global-environments-with-project-install: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: ./ + with: + run-install: false + global-environments: | + cowpy + keyring --with keyrings.google-artifactregistry-auth + - run: | + set -o pipefail + cowpy hello world + keyring --list-backends | grep "keyrings.gauth.GooglePythonAuth" + shell: bash + if: matrix.os != 'windows-latest' + - run: | + cowpy hello world + keyring --list-backends | findstr "keyrings.gauth.GooglePythonAuth" + if: matrix.os == 'windows-latest' + no-run-install: strategy: matrix: diff --git a/README.md b/README.md index 721d446..7c1d52c 100644 --- a/README.md +++ b/README.md @@ -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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 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.0`) to avoid breaking changes. +> Please pin the versions of this action to a specific version (i.e., `prefix-dev/setup-pixi@v0.9.1`) 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: @@ -74,7 +74,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: cache: true cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }} @@ -119,7 +119,7 @@ test: environment: [py311, py312] steps: - uses: actions/checkout@v4 - - uses: prefix-dev/setup-pixi@v0.9.0 + - uses: prefix-dev/setup-pixi@v0.9.1 with: environments: ${{ matrix.environment }} ``` @@ -129,7 +129,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: # separated by spaces environments: >- @@ -143,14 +143,34 @@ The following example will install both the `py311` and the `py312` environment > [!WARNING] > If you don't specify any environment, the `default` environment will be installed and cached, even if you use other environments. +### Global Environments + +You can specify `pixi global install` commands by setting the `global-environments` input argument. +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. + +```yml +- uses: prefix-dev/setup-pixi@v0.9.1 + with: + global-environments: | + google-cloud-sdk + keyring --with keyrings.google-artifactregistry-auth +- run: | + gcloud --version + keyring --list-backends +``` + ### Authentication -There are currently three ways to authenticate with pixi: +There are currently five ways to authenticate with pixi: - using a token - using a username and password - using a conda-token - using an S3 key pair +- using keyring for PyPI registries For more information, see the [pixi documentation](https://prefix.dev/docs/pixi/authentication). @@ -165,7 +185,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: auth-host: prefix.dev auth-token: ${{ secrets.PREFIX_DEV_TOKEN }} @@ -177,7 +197,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: auth-host: custom-artifactory.com auth-username: ${{ secrets.PIXI_USERNAME }} @@ -190,7 +210,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//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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: auth-host: anaconda.org # or my-quetz-instance.com auth-conda-token: ${{ secrets.CONDA_TOKEN }} @@ -202,7 +222,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: auth-host: s3://my-s3-bucket auth-s3-access-key-id: ${{ secrets.ACCESS_KEY_ID }} @@ -218,11 +238,13 @@ 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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: pypi-keyring-provider: subprocess # one of 'subprocess', 'disabled' ``` +You can use the [`global-environments`](#global-environments) input to install `keyring` and its backends. + ### Custom shell wrapper `setup-pixi` allows you to run command inside of the pixi environment by specifying a custom shell wrapper with `shell: pixi run bash -e {0}`. @@ -284,7 +306,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: activate-environment: true ``` @@ -292,7 +314,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: environments: >- py311 @@ -309,7 +331,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: locked: true # or @@ -328,7 +350,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 env: RUNNER_DEBUG: true ``` @@ -346,7 +368,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: # one of `q`, `default`, `v`, `vv`, or `vvv`. log-level: vvv @@ -372,7 +394,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: post-cleanup: true # ${{ runner.temp }}\Scripts\pixi.exe on Windows @@ -388,7 +410,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: manifest-path: pyproject.toml ``` @@ -398,7 +420,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: run-install: false ``` @@ -409,7 +431,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 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 }}"}' @@ -425,7 +447,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.0 +- uses: prefix-dev/setup-pixi@v0.9.1 with: pixi-url: | {{#if latest~}} diff --git a/action.yml b/action.yml index 8f42937..e3e9975 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,9 @@ inputs: environments: description: | A space-separated list of environments to install. If not specified, only the default environment is installed. + global-environments: + description: | + A newline-separated list of packages to install globally with `pixi global install`. activate-environment: description: | If the installed environment should be "activated" for the current job, modifying `$GITHUB_ENV` and diff --git a/dist/index.js b/dist/index.js index eecdf65..58b5574 100644 --- a/dist/index.js +++ b/dist/index.js @@ -22996,11 +22996,11 @@ var require_runtime = __commonJS({ depths = [context]; } } - function main(context2) { + function main2(context2) { return "" + templateSpec.main(container, context2, container.helpers, container.partials, data, blockParams, depths); } - main = executeDecorators(templateSpec.main, main, container, options2.depths || [], data, blockParams); - return main(context, options2); + main2 = executeDecorators(templateSpec.main, main2, container, options2.depths || [], data, blockParams); + return main2(context, options2); } ret.isTop = true; ret._setup = function(options2) { @@ -79803,6 +79803,13 @@ var parseOrUndefinedList = (key, schema) => { } return input.split(" ").map((s) => schema.parse(s)).filter((s) => s !== ""); }; +var parseOrUndefinedMultilineList = (key, schema) => { + const input = inputOrEnvironmentVariable(key); + if (input === void 0) { + return void 0; + } + return input.split("\n").map((s) => schema.parse(s.trim())).filter((s) => s !== ""); +}; var validateInputs = (inputs) => { if (inputs.pixiUrlHeaders && !inputs.pixiUrl) { throw new Error("You need to specify pixi-url when using pixi-url-headers"); @@ -79955,6 +79962,7 @@ var inferOptions = (inputs) => { const postCleanup = inputs.postCleanup ?? true; const pypiKeyringProvider = inputs.pypiKeyringProvider; return { + globalEnvironments: inputs.globalEnvironments, pixiSource, pypiKeyringProvider, downloadPixi: downloadPixi2, @@ -80007,6 +80015,7 @@ var getOptions = () => { authS3SecretAccessKey: parseOrUndefined("auth-s3-secret-access-key", string2()), authS3SessionToken: parseOrUndefined("auth-s3-session-token", string2()), pypiKeyringProvider: parseOrUndefined("pypi-keyring-provider", pypiKeyringProviderSchema), + globalEnvironments: parseOrUndefinedMultilineList("global-environments", string2()), postCleanup: parseOrUndefinedJSON("post-cleanup", boolean2()) }; core2.debug(`Inputs: ${JSON.stringify(inputs)}`); @@ -80157,81 +80166,93 @@ var activateEnvironment = async (environment) => { }; // src/main.ts -var downloadPixi = (source) => { +var downloadPixi = async (source) => { const url = renderPixiUrl(source.urlTemplate, source.version); - return core5.group("Downloading Pixi", () => { + await core5.group("Downloading Pixi", async () => { core5.debug("Installing pixi"); core5.debug(`Downloading pixi from ${url}`); core5.debug(`Using headers: ${JSON.stringify(source.headers)}`); - return import_promises2.default.mkdir(import_path3.default.dirname(options.pixiBinPath), { recursive: true }).then(() => (0, import_tool_cache.downloadTool)(url, options.pixiBinPath, void 0, source.headers)).then((_downloadPath) => import_promises2.default.chmod(options.pixiBinPath, 493)).then(() => { - core5.info(`Pixi installed to ${options.pixiBinPath}`); - }); + await import_promises2.default.mkdir(import_path3.default.dirname(options.pixiBinPath), { recursive: true }); + await (0, import_tool_cache.downloadTool)(url, options.pixiBinPath, void 0, source.headers); + await import_promises2.default.chmod(options.pixiBinPath, 493); + core5.info(`Pixi installed to ${options.pixiBinPath}`); }); }; -var pixiLogin = () => { +var pixiLogin = async () => { const auth = options.auth; if (!auth) { core5.debug("Skipping pixi login."); - return Promise.resolve(0); + return; } core5.debug(`auth keys: ${Object.keys(auth).toString()}`); - return core5.group("Logging in to private channel", () => { + await core5.group("Logging in to private channel", async () => { if ("token" in auth) { core5.debug(`Logging in to ${auth.host} with token`); - return execute(pixiCmd(`auth login --token ${auth.token} ${auth.host}`, false)); - } - if ("username" in auth) { + await execute(pixiCmd(`auth login --token ${auth.token} ${auth.host}`, false)); + } else if ("username" in auth) { core5.debug(`Logging in to ${auth.host} with username and password`); - return execute(pixiCmd(`auth login --username ${auth.username} --password ${auth.password} ${auth.host}`, false)); - } - if ("s3AccessKeyId" in auth) { + await execute(pixiCmd(`auth login --username ${auth.username} --password ${auth.password} ${auth.host}`, false)); + } else if ("s3AccessKeyId" in auth) { core5.debug(`Logging in to ${auth.host} with s3 credentials`); const command = auth.s3SessionToken ? `auth login --s3-access-key-id ${auth.s3AccessKeyId} --s3-secret-access-key ${auth.s3SecretAccessKey} --s3-session-token ${auth.s3SessionToken} ${auth.host}` : `auth login --s3-access-key-id ${auth.s3AccessKeyId} --s3-secret-access-key ${auth.s3SecretAccessKey} ${auth.host}`; - return execute(pixiCmd(command, false)); + await execute(pixiCmd(command, false)); + } else if ("condaToken" in auth) { + core5.debug(`Logging in to ${auth.host} with conda token`); + await execute(pixiCmd(`auth login --conda-token ${auth.condaToken} ${auth.host}`, false)); } - core5.debug(`Logging in to ${auth.host} with conda token`); - return execute(pixiCmd(`auth login --conda-token ${auth.condaToken} ${auth.host}`, false)); }); }; var addPixiToPath = () => { core5.addPath(import_path3.default.dirname(options.pixiBinPath)); }; +var pixiGlobalInstall = async () => { + const { globalEnvironments } = options; + if (!globalEnvironments) { + core5.debug("Skipping pixi global install."); + return; + } + core5.debug("Installing global environments"); + for (const env of globalEnvironments) { + const command = `global install ${env}`; + await core5.group(`pixi ${command}`, () => execute(pixiCmd(command, false))); + } +}; var pixiInstall = async () => { if (!options.runInstall) { core5.debug("Skipping pixi install."); - return Promise.resolve(); + return; } - return tryRestoreCache().then(async (_cacheKey) => { - const environments = options.environments ?? [void 0]; - for (const environment of environments) { - core5.debug(`Installing environment ${environment ?? "default"}`); - let command = `install`; - if (environment) { - command += ` -e ${environment}`; - } - if (options.frozen) { - command += " --frozen"; - } - if (options.locked) { - command += " --locked"; - } - if (options.pypiKeyringProvider) { - command += ` --pypi-keyring-provider ${options.pypiKeyringProvider}`; - } - await core5.group(`pixi ${command}`, () => execute(pixiCmd(command))); + await tryRestoreCache(); + const environments = options.environments ?? [void 0]; + for (const environment of environments) { + core5.debug(`Installing environment ${environment ?? "default"}`); + let command = `install`; + if (environment) { + command += ` -e ${environment}`; + } + if (options.frozen) { + command += " --frozen"; } - }).then(saveCache2); + if (options.locked) { + command += " --locked"; + } + if (options.pypiKeyringProvider) { + command += ` --pypi-keyring-provider ${options.pypiKeyringProvider}`; + } + await core5.group(`pixi ${command}`, () => execute(pixiCmd(command))); + } + await saveCache2(); }; var generateList = async () => { if (!options.runInstall) { core5.debug("Skipping pixi list."); - return Promise.resolve(); + return; } if ("version" in options.pixiSource && options.pixiSource.version !== "latest" && options.pixiSource.version < "v0.13.0") { core5.warning( "pixi list is not supported for pixi versions < `v0.13.0`. Please set `pixi-version` to `v0.13.0` or later." ); - return Promise.resolve(); + return; } let command = "list"; if ("version" in options.pixiSource && options.pixiSource.version !== "latest" && options.pixiSource.version < "v0.14.0") { @@ -80243,15 +80264,19 @@ var generateList = async () => { if (options.environments) { for (const environment of options.environments) { core5.debug(`Listing environment ${environment}`); - await core5.group(`pixi ${command} -e ${environment}`, () => execute(pixiCmd(`${command} -e ${environment}`))); + const cmd = `${command} -e ${environment}`; + await core5.group(`pixi ${cmd}`, () => execute(pixiCmd(cmd))); } - return Promise.resolve(); } else { - return core5.group(`pixi ${command}`, () => execute(pixiCmd(command))); + await core5.group(`pixi ${command}`, () => execute(pixiCmd(command))); } }; -var generateInfo = () => core5.group("pixi info", () => execute(pixiCmd("info"))); -var activateEnv = (environment) => core5.group("Activate environment", () => activateEnvironment(environment)); +var generateInfo = async () => { + await core5.group("pixi info", () => execute(pixiCmd("info"))); +}; +var activateEnv = async (environment) => { + await core5.group("Activate environment", () => activateEnvironment(environment)); +}; var run = async () => { core5.debug(`process.env.HOME: ${process.env.HOME ?? "-"}`); core5.debug(`os.homedir(): ${import_os4.default.homedir()}`); @@ -80260,6 +80285,7 @@ var run = async () => { } addPixiToPath(); await pixiLogin(); + await pixiGlobalInstall(); await pixiInstall(); await generateInfo(); await generateList(); @@ -80267,19 +80293,26 @@ var run = async () => { await activateEnv(options.activatedEnvironment); } }; -run().then(() => (0, import_process2.exit)(0)).catch((error3) => { - if (core5.isDebug()) { - throw error3; - } - if (error3 instanceof Error) { - core5.setFailed(error3.message); - (0, import_process2.exit)(1); - } else if (typeof error3 === "string") { - core5.setFailed(error3); - (0, import_process2.exit)(1); +var main = async () => { + try { + await run(); + (0, import_process2.exit)(0); + } catch (error3) { + if (core5.isDebug()) { + throw error3; + } + if (error3 instanceof Error) { + core5.setFailed(error3.message); + (0, import_process2.exit)(1); + } else if (typeof error3 === "string") { + core5.setFailed(error3); + (0, import_process2.exit)(1); + } else { + throw error3; + } } - throw error3; -}); +}; +void main(); /*! Bundled license information: undici/lib/fetch/body.js: diff --git a/dist/post.js b/dist/post.js index 29db7bb..c3e07fa 100644 --- a/dist/post.js +++ b/dist/post.js @@ -29891,6 +29891,13 @@ var parseOrUndefinedList = (key, schema) => { } return input.split(" ").map((s) => schema.parse(s)).filter((s) => s !== ""); }; +var parseOrUndefinedMultilineList = (key, schema) => { + const input = inputOrEnvironmentVariable(key); + if (input === void 0) { + return void 0; + } + return input.split("\n").map((s) => schema.parse(s.trim())).filter((s) => s !== ""); +}; var validateInputs = (inputs) => { if (inputs.pixiUrlHeaders && !inputs.pixiUrl) { throw new Error("You need to specify pixi-url when using pixi-url-headers"); @@ -30043,6 +30050,7 @@ var inferOptions = (inputs) => { const postCleanup = inputs.postCleanup ?? true; const pypiKeyringProvider = inputs.pypiKeyringProvider; return { + globalEnvironments: inputs.globalEnvironments, pixiSource, pypiKeyringProvider, downloadPixi, @@ -30095,6 +30103,7 @@ var getOptions = () => { authS3SecretAccessKey: parseOrUndefined("auth-s3-secret-access-key", string2()), authS3SessionToken: parseOrUndefined("auth-s3-session-token", string2()), pypiKeyringProvider: parseOrUndefined("pypi-keyring-provider", pypiKeyringProviderSchema), + globalEnvironments: parseOrUndefinedMultilineList("global-environments", string2()), postCleanup: parseOrUndefinedJSON("post-cleanup", boolean2()) }; core2.debug(`Inputs: ${JSON.stringify(inputs)}`); diff --git a/package.json b/package.json index 503a589..e5d74ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setup-pixi", - "version": "0.9.0", + "version": "0.9.1", "private": true, "description": "Action to set up the pixi package manager.", "scripts": { diff --git a/src/main.ts b/src/main.ts index da73cf2..ea14414 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,48 +10,44 @@ import { execute, pixiCmd, renderPixiUrl } from './util' import { saveCache, tryRestoreCache } from './cache' import { activateEnvironment } from './activate' -const downloadPixi = (source: PixiSource) => { +const downloadPixi = async (source: PixiSource) => { const url = renderPixiUrl(source.urlTemplate, source.version) - return core.group('Downloading Pixi', () => { + await core.group('Downloading Pixi', async () => { core.debug('Installing pixi') core.debug(`Downloading pixi from ${url}`) core.debug(`Using headers: ${JSON.stringify(source.headers)}`) - return fs - .mkdir(path.dirname(options.pixiBinPath), { recursive: true }) - .then(() => downloadTool(url, options.pixiBinPath, undefined, source.headers)) - .then((_downloadPath) => fs.chmod(options.pixiBinPath, 0o755)) - .then(() => { - core.info(`Pixi installed to ${options.pixiBinPath}`) - }) + await fs.mkdir(path.dirname(options.pixiBinPath), { recursive: true }) + await downloadTool(url, options.pixiBinPath, undefined, source.headers) + await fs.chmod(options.pixiBinPath, 0o755) + core.info(`Pixi installed to ${options.pixiBinPath}`) }) } -const pixiLogin = () => { +const pixiLogin = async () => { const auth = options.auth if (!auth) { core.debug('Skipping pixi login.') - return Promise.resolve(0) + return } core.debug(`auth keys: ${Object.keys(auth).toString()}`) - return core.group('Logging in to private channel', () => { + await core.group('Logging in to private channel', async () => { // tokens get censored in the logs as long as they are a github secret if ('token' in auth) { core.debug(`Logging in to ${auth.host} with token`) - return execute(pixiCmd(`auth login --token ${auth.token} ${auth.host}`, false)) - } - if ('username' in auth) { + await execute(pixiCmd(`auth login --token ${auth.token} ${auth.host}`, false)) + } else if ('username' in auth) { core.debug(`Logging in to ${auth.host} with username and password`) - return execute(pixiCmd(`auth login --username ${auth.username} --password ${auth.password} ${auth.host}`, false)) - } - if ('s3AccessKeyId' in auth) { + await execute(pixiCmd(`auth login --username ${auth.username} --password ${auth.password} ${auth.host}`, false)) + } else if ('s3AccessKeyId' in auth) { core.debug(`Logging in to ${auth.host} with s3 credentials`) const command = auth.s3SessionToken ? `auth login --s3-access-key-id ${auth.s3AccessKeyId} --s3-secret-access-key ${auth.s3SecretAccessKey} --s3-session-token ${auth.s3SessionToken} ${auth.host}` : `auth login --s3-access-key-id ${auth.s3AccessKeyId} --s3-secret-access-key ${auth.s3SecretAccessKey} ${auth.host}` - return execute(pixiCmd(command, false)) + await execute(pixiCmd(command, false)) + } else if ('condaToken' in auth) { + core.debug(`Logging in to ${auth.host} with conda token`) + await execute(pixiCmd(`auth login --conda-token ${auth.condaToken} ${auth.host}`, false)) } - core.debug(`Logging in to ${auth.host} with conda token`) - return execute(pixiCmd(`auth login --conda-token ${auth.condaToken} ${auth.host}`, false)) }) } @@ -59,39 +55,53 @@ const addPixiToPath = () => { core.addPath(path.dirname(options.pixiBinPath)) } +const pixiGlobalInstall = async () => { + const { globalEnvironments } = options + if (!globalEnvironments) { + core.debug('Skipping pixi global install.') + return + } + core.debug('Installing global environments') + for (const env of globalEnvironments) { + const command = `global install ${env}` + await core.group(`pixi ${command}`, () => execute(pixiCmd(command, false))) + } +} + const pixiInstall = async () => { if (!options.runInstall) { core.debug('Skipping pixi install.') - return Promise.resolve() + return } - return tryRestoreCache() - .then(async (_cacheKey) => { - const environments = options.environments ?? [undefined] - for (const environment of environments) { - core.debug(`Installing environment ${environment ?? 'default'}`) - let command = `install` - if (environment) { - command += ` -e ${environment}` - } - if (options.frozen) { - command += ' --frozen' - } - if (options.locked) { - command += ' --locked' - } - if (options.pypiKeyringProvider) { - command += ` --pypi-keyring-provider ${options.pypiKeyringProvider}` - } - await core.group(`pixi ${command}`, () => execute(pixiCmd(command))) - } - }) - .then(saveCache) + + await tryRestoreCache() + + const environments = options.environments ?? [undefined] + for (const environment of environments) { + core.debug(`Installing environment ${environment ?? 'default'}`) + let command = `install` + if (environment) { + command += ` -e ${environment}` + } + if (options.frozen) { + command += ' --frozen' + } + if (options.locked) { + command += ' --locked' + } + if (options.pypiKeyringProvider) { + command += ` --pypi-keyring-provider ${options.pypiKeyringProvider}` + } + await core.group(`pixi ${command}`, () => execute(pixiCmd(command))) + } + + await saveCache() } const generateList = async () => { if (!options.runInstall) { core.debug('Skipping pixi list.') - return Promise.resolve() + return } if ( 'version' in options.pixiSource && @@ -101,7 +111,7 @@ const generateList = async () => { core.warning( 'pixi list is not supported for pixi versions < `v0.13.0`. Please set `pixi-version` to `v0.13.0` or later.' ) - return Promise.resolve() + return } let command = 'list' if ( @@ -117,17 +127,21 @@ const generateList = async () => { if (options.environments) { for (const environment of options.environments) { core.debug(`Listing environment ${environment}`) - await core.group(`pixi ${command} -e ${environment}`, () => execute(pixiCmd(`${command} -e ${environment}`))) + const cmd = `${command} -e ${environment}` + await core.group(`pixi ${cmd}`, () => execute(pixiCmd(cmd))) } - return Promise.resolve() } else { - return core.group(`pixi ${command}`, () => execute(pixiCmd(command))) + await core.group(`pixi ${command}`, () => execute(pixiCmd(command))) } } -const generateInfo = () => core.group('pixi info', () => execute(pixiCmd('info'))) +const generateInfo = async () => { + await core.group('pixi info', () => execute(pixiCmd('info'))) +} -const activateEnv = (environment: string) => core.group('Activate environment', () => activateEnvironment(environment)) +const activateEnv = async (environment: string) => { + await core.group('Activate environment', () => activateEnvironment(environment)) +} const run = async () => { core.debug(`process.env.HOME: ${process.env.HOME ?? '-'}`) @@ -137,6 +151,7 @@ const run = async () => { } addPixiToPath() await pixiLogin() + await pixiGlobalInstall() await pixiInstall() await generateInfo() await generateList() @@ -145,9 +160,12 @@ const run = async () => { } } -run() - .then(() => exit(0)) // workaround for https://github.com/actions/toolkit/issues/1578 - .catch((error: unknown) => { +const main = async () => { + try { + await run() + // workaround for https://github.com/actions/toolkit/issues/1578 + exit(0) + } catch (error: unknown) { if (core.isDebug()) { throw error } @@ -157,6 +175,10 @@ run() } else if (typeof error === 'string') { core.setFailed(error) exit(1) + } else { + throw error } - throw error - }) + } +} + +void main() diff --git a/src/options.ts b/src/options.ts index 4426735..7a58c2f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -34,6 +34,7 @@ type Inputs = Readonly<{ authS3SessionToken?: string pypiKeyringProvider?: 'disabled' | 'subprocess' postCleanup?: boolean + globalEnvironments?: string[] }> export interface PixiSource { @@ -83,6 +84,7 @@ export type Options = Readonly<{ pypiKeyringProvider?: 'disabled' | 'subprocess' postCleanup: boolean activatedEnvironment?: string + globalEnvironments?: string[] }> const pixiPath = 'pixi.toml' const pyprojectPath = 'pyproject.toml' @@ -152,6 +154,17 @@ const parseOrUndefinedList = (key: string, schema: z.ZodType): T[] | undef .filter((s) => s !== '') } +const parseOrUndefinedMultilineList = (key: string, schema: z.ZodType): T[] | undefined => { + const input = inputOrEnvironmentVariable(key) + if (input === undefined) { + return undefined + } + return input + .split('\n') + .map((s) => schema.parse(s.trim())) + .filter((s) => s !== '') +} + const validateInputs = (inputs: Inputs): void => { if (inputs.pixiUrlHeaders && !inputs.pixiUrl) { throw new Error('You need to specify pixi-url when using pixi-url-headers') @@ -330,6 +343,7 @@ const inferOptions = (inputs: Inputs): Options => { const postCleanup = inputs.postCleanup ?? true const pypiKeyringProvider = inputs.pypiKeyringProvider return { + globalEnvironments: inputs.globalEnvironments, pixiSource, pypiKeyringProvider, downloadPixi, @@ -390,6 +404,7 @@ const getOptions = () => { authS3SecretAccessKey: parseOrUndefined('auth-s3-secret-access-key', z.string()), authS3SessionToken: parseOrUndefined('auth-s3-session-token', z.string()), pypiKeyringProvider: parseOrUndefined('pypi-keyring-provider', pypiKeyringProviderSchema), + globalEnvironments: parseOrUndefinedMultilineList('global-environments', z.string()), postCleanup: parseOrUndefinedJSON('post-cleanup', z.boolean()) } core.debug(`Inputs: ${JSON.stringify(inputs)}`)