feat: Add persist-credentials option (#266)

Co-authored-by: Pavel Zwerschke <pavelzw@gmail.com>
This commit is contained in:
Andreas Albert
2026-05-21 13:13:17 +02:00
committed by GitHub
parent 92596c33b0
commit 5185adfbff
9 changed files with 169 additions and 37 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
run: |
set -euo pipefail
latest_version="$(jq -r '.version' package.json)"
count_expected=23
count_expected=24
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."
+46
View File
@@ -646,6 +646,52 @@ jobs:
# https://github.com/prefix-dev/pixi/issues/330
if: matrix.os == 'ubuntu-latest'
persist-credentials-false:
env:
# We must set this environment variable explicitly to force all
# operating systems to use the same storage mechanism.
# Otherwise, windows and mac will use the keychain, which is more cumbersome to test.
RATTLER_AUTH_FILE: .rattler-credentials.json
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Move pixi.toml
run: mv test/default/* .
# Sanity check: Login with default persist-credentials behavior
- uses: ./
with:
cache: false
auth-host: https://custom-conda-registry.com
auth-token: custom-token
- name: Assert that the credentials are stored
run: |
# For human log readers
cat "${RATTLER_AUTH_FILE}"
# Actual test
[ $(jq '."*.custom-conda-registry.com".BearerToken' -r "${RATTLER_AUTH_FILE}") = "custom-token" ]
- name: Clean up credentials file
run: rm "${RATTLER_AUTH_FILE}"
# Actual test: Login with persist-credentials: false
- uses: ./
with:
cache: false
auth-host: https://custom-conda-registry.com
auth-token: custom-token
persist-credentials: false
- name: Assert that the credentials are not stored anymore
run: |
# For human log readers
cat "${RATTLER_AUTH_FILE}"
# Actual test
! grep -q '"*.custom-conda-registry.com"' "${RATTLER_AUTH_FILE}"
auth-token-install:
strategy:
matrix:
+38 -23
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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
pixi-version: v0.66.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.5`) to avoid breaking changes.
> Please pin the versions of this action to a specific version (i.e., `prefix-dev/setup-pixi@v0.9.6`) 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:
@@ -79,7 +79,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
cache: true
cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
@@ -124,7 +124,7 @@ test:
environment: [py311, py312]
steps:
- uses: actions/checkout@v4
- uses: prefix-dev/setup-pixi@v0.9.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
environments: ${{ matrix.environment }}
```
@@ -134,7 +134,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
# separated by spaces
environments: >-
@@ -157,7 +157,7 @@ For instance, the `keyring`, or `gcloud` executables. The following example show
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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
global-environments: |
google-cloud-sdk
@@ -190,7 +190,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
auth-host: prefix.dev
auth-token: ${{ secrets.PREFIX_DEV_TOKEN }}
@@ -202,7 +202,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
auth-host: custom-artifactory.com
auth-username: ${{ secrets.PIXI_USERNAME }}
@@ -215,7 +215,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
auth-host: anaconda.org # or my-quetz-instance.com
auth-conda-token: ${{ secrets.CONDA_TOKEN }}
@@ -227,7 +227,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
auth-host: s3://my-s3-bucket
auth-s3-access-key-id: ${{ secrets.ACCESS_KEY_ID }}
@@ -238,12 +238,27 @@ You can also specify the session token using the `auth-session-token` input argu
See the [pixi documentation](https://pixi.sh/latest/advanced/s3) for more information about S3 authentication.
#### Restricting credentials to the install step
If you only want pixi to use the authenticated remote channel during the action's own install step
(and not in any subsequent step of the workflow), set `persist-credentials: false`. The action will
then run `pixi auth logout <auth-host>` after `pixi install` has completed but before the action
returns, so that later steps cannot reach the private channel anymore.
```yml
- uses: prefix-dev/setup-pixi@v0.9.6
with:
auth-host: prefix.dev
auth-token: ${{ secrets.PREFIX_DEV_TOKEN }}
persist-credentials: false
```
#### PyPI keyring provider
You can specify whether to use keyring to look up credentials for PyPI.
```yml
- uses: prefix-dev/setup-pixi@v0.9.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
pypi-keyring-provider: subprocess # one of 'subprocess', 'disabled'
```
@@ -311,7 +326,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
activate-environment: true
```
@@ -319,7 +334,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
environments: >-
py311
@@ -336,7 +351,7 @@ You can specify whether `setup-pixi` should run `pixi install --frozen` or `pixi
See the [official documentation](https://pixi.sh/latest/reference/cli/pixi/install/#update-options) for more information about the `--frozen` and `--locked` flags.
```yml
- uses: prefix-dev/setup-pixi@v0.9.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
locked: true
# or
@@ -355,7 +370,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
env:
RUNNER_DEBUG: true
```
@@ -373,7 +388,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
# one of `q`, `default`, `v`, `vv`, or `vvv`.
log-level: vvv
@@ -399,7 +414,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
post-cleanup: true
# ${{ runner.temp }}\Scripts\pixi.exe on Windows
@@ -415,7 +430,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
manifest-path: pyproject.toml
```
@@ -425,7 +440,7 @@ This can be overwritten by setting the `manifest-path` input argument.
If you're working with a monorepo where your pixi project is in a subdirectory, you can use the `working-directory` input to specify where pixi should look for manifest files (`pixi.toml` or `pyproject.toml`).
```yml
- uses: prefix-dev/setup-pixi@v0.9.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
working-directory: ./packages/my-project
```
@@ -444,7 +459,7 @@ This will make pixi look for `pixi.toml` or `pyproject.toml` in the `./packages/
You can combine `working-directory` with `manifest-path` if needed:
```yml
- uses: prefix-dev/setup-pixi@v0.9.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
working-directory: ./packages/my-project
manifest-path: custom-pixi.toml
@@ -455,7 +470,7 @@ You can combine `working-directory` with `manifest-path` if needed:
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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
run-install: false
```
@@ -466,7 +481,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
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 }}"}'
@@ -482,7 +497,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.5
- uses: prefix-dev/setup-pixi@v0.9.6
with:
pixi-url: |
{{#if latest~}}
+6
View File
@@ -78,6 +78,12 @@ inputs:
description: Secret access key to use for S3 authentication.
auth-s3-session-token:
description: Session token to use for S3 authentication.
persist-credentials:
description: |
Whether to keep the credentials configured by `auth-host` available to subsequent workflow
steps. Defaults to `true`. If set to `false`, the action runs `pixi auth logout <auth-host>`
after install, so that later steps cannot reach the private channel anymore. Requires
`auth-host`.
pypi-keyring-provider:
description: |
Specifies whether to use keyring to look up credentials for PyPI.
Generated Vendored
+26 -4
View File
@@ -54582,6 +54582,9 @@ var validateInputs = (inputs) => {
if (inputs.authToken || inputs.authUsername || inputs.authCondaToken || inputs.authS3AccessKeyId) {
throw new Error("You need to specify auth-host");
}
if (inputs.persistCredentials === false) {
throw new Error("Cannot use persist-credentials without specifying auth-host");
}
}
if (inputs.runInstall === false && inputs.environments) {
throw new Error("Cannot specify environments without running install");
@@ -54674,21 +54677,26 @@ var inferOptions = (inputs) => {
} : void 0;
const frozen = inputs.frozen ?? false;
const locked = inputs.locked ?? (lockFileAvailable && !frozen);
const persistCredentials = inputs.persistCredentials ?? true;
const auth = !inputs.authHost ? void 0 : inputs.authToken ? {
host: inputs.authHost,
token: inputs.authToken
token: inputs.authToken,
persistCredentials
} : inputs.authCondaToken ? {
host: inputs.authHost,
condaToken: inputs.authCondaToken
condaToken: inputs.authCondaToken,
persistCredentials
} : inputs.authUsername ? {
host: inputs.authHost,
username: inputs.authUsername,
password: inputs.authPassword
password: inputs.authPassword,
persistCredentials
} : {
host: inputs.authHost,
s3AccessKeyId: inputs.authS3AccessKeyId,
s3SecretAccessKey: inputs.authS3SecretAccessKey,
s3SessionToken: inputs.authS3SessionToken
s3SessionToken: inputs.authS3SessionToken,
persistCredentials
};
const postCleanup = inputs.postCleanup ?? true;
const pypiKeyringProvider = inputs.pypiKeyringProvider;
@@ -54710,6 +54718,7 @@ var inferOptions = (inputs) => {
globalCache,
pixiBinPath,
auth,
persistCredentials,
postCleanup
};
};
@@ -54750,6 +54759,7 @@ var getOptions = () => {
authS3AccessKeyId: parseOrUndefined("auth-s3-access-key-id", string2()),
authS3SecretAccessKey: parseOrUndefined("auth-s3-secret-access-key", string2()),
authS3SessionToken: parseOrUndefined("auth-s3-session-token", string2()),
persistCredentials: parseOrUndefinedJSON("persist-credentials", boolean2()),
pypiKeyringProvider: parseOrUndefined("pypi-keyring-provider", pypiKeyringProviderSchema),
globalEnvironments: parseOrUndefinedMultilineList("global-environments", string2()),
postCleanup: parseOrUndefinedJSON("post-cleanup", boolean2())
@@ -89877,6 +89887,17 @@ var pixiLogin = async () => {
}
});
};
var pixiLogout = async () => {
const auth = options.auth;
if (!auth || auth.persistCredentials) {
debug("Skipping pixi logout.");
return;
}
await group("Logging out of private channel", async () => {
debug(`Logging out of ${auth.host}`);
await execute(pixiCmd(`auth logout ${auth.host}`, false));
});
};
var addPixiToPath = () => {
addPath(import_path3.default.dirname(options.pixiBinPath));
};
@@ -89969,6 +89990,7 @@ var run = async () => {
if (options.activatedEnvironment) {
await activateEnv(options.activatedEnvironment);
}
await pixiLogout();
};
var main = async () => {
try {
Generated Vendored
+14 -4
View File
@@ -30143,6 +30143,9 @@ var validateInputs = (inputs) => {
if (inputs.authToken || inputs.authUsername || inputs.authCondaToken || inputs.authS3AccessKeyId) {
throw new Error("You need to specify auth-host");
}
if (inputs.persistCredentials === false) {
throw new Error("Cannot use persist-credentials without specifying auth-host");
}
}
if (inputs.runInstall === false && inputs.environments) {
throw new Error("Cannot specify environments without running install");
@@ -30235,21 +30238,26 @@ var inferOptions = (inputs) => {
} : void 0;
const frozen = inputs.frozen ?? false;
const locked = inputs.locked ?? (lockFileAvailable && !frozen);
const persistCredentials = inputs.persistCredentials ?? true;
const auth = !inputs.authHost ? void 0 : inputs.authToken ? {
host: inputs.authHost,
token: inputs.authToken
token: inputs.authToken,
persistCredentials
} : inputs.authCondaToken ? {
host: inputs.authHost,
condaToken: inputs.authCondaToken
condaToken: inputs.authCondaToken,
persistCredentials
} : inputs.authUsername ? {
host: inputs.authHost,
username: inputs.authUsername,
password: inputs.authPassword
password: inputs.authPassword,
persistCredentials
} : {
host: inputs.authHost,
s3AccessKeyId: inputs.authS3AccessKeyId,
s3SecretAccessKey: inputs.authS3SecretAccessKey,
s3SessionToken: inputs.authS3SessionToken
s3SessionToken: inputs.authS3SessionToken,
persistCredentials
};
const postCleanup = inputs.postCleanup ?? true;
const pypiKeyringProvider = inputs.pypiKeyringProvider;
@@ -30271,6 +30279,7 @@ var inferOptions = (inputs) => {
globalCache,
pixiBinPath,
auth,
persistCredentials,
postCleanup
};
};
@@ -30311,6 +30320,7 @@ var getOptions = () => {
authS3AccessKeyId: parseOrUndefined("auth-s3-access-key-id", string2()),
authS3SecretAccessKey: parseOrUndefined("auth-s3-secret-access-key", string2()),
authS3SessionToken: parseOrUndefined("auth-s3-session-token", string2()),
persistCredentials: parseOrUndefinedJSON("persist-credentials", boolean2()),
pypiKeyringProvider: parseOrUndefined("pypi-keyring-provider", pypiKeyringProviderSchema),
globalEnvironments: parseOrUndefinedMultilineList("global-environments", string2()),
postCleanup: parseOrUndefinedJSON("post-cleanup", boolean2())
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "setup-pixi",
"version": "0.9.5",
"version": "0.9.6",
"private": true,
"description": "Action to set up the pixi package manager.",
"scripts": {
+13
View File
@@ -51,6 +51,18 @@ const pixiLogin = async () => {
})
}
const pixiLogout = async () => {
const auth = options.auth
if (!auth || auth.persistCredentials) {
core.debug('Skipping pixi logout.')
return
}
await core.group('Logging out of private channel', async () => {
core.debug(`Logging out of ${auth.host}`)
await execute(pixiCmd(`auth logout ${auth.host}`, false))
})
}
const addPixiToPath = () => {
core.addPath(path.dirname(options.pixiBinPath))
}
@@ -163,6 +175,7 @@ const run = async () => {
if (options.activatedEnvironment) {
await activateEnv(options.activatedEnvironment)
}
await pixiLogout()
}
const main = async () => {
+24 -4
View File
@@ -35,6 +35,7 @@ type Inputs = Readonly<{
authS3AccessKeyId?: string
authS3SecretAccessKey?: string
authS3SessionToken?: string
persistCredentials?: boolean
pypiKeyringProvider?: 'disabled' | 'subprocess'
postCleanup?: boolean
globalEnvironments?: string[]
@@ -48,6 +49,7 @@ export interface PixiSource {
type Auth = {
host: string
persistCredentials: boolean
} & (
| {
token: string
@@ -91,6 +93,7 @@ export type Options = Readonly<{
globalCache?: GlobalCache
pixiBinPath: string
auth?: Auth
persistCredentials: boolean
pypiKeyringProvider?: 'disabled' | 'subprocess'
postCleanup: boolean
activatedEnvironment?: string
@@ -240,6 +243,9 @@ const validateInputs = (inputs: Inputs): void => {
if (inputs.authToken || inputs.authUsername || inputs.authCondaToken || inputs.authS3AccessKeyId) {
throw new Error('You need to specify auth-host')
}
if (inputs.persistCredentials === false) {
throw new Error('Cannot use persist-credentials without specifying auth-host')
}
}
if (inputs.runInstall === false && inputs.environments) {
throw new Error('Cannot specify environments without running install')
@@ -355,29 +361,34 @@ const inferOptions = (inputs: Inputs): Options => {
: undefined
const frozen = inputs.frozen ?? false
const locked = inputs.locked ?? (lockFileAvailable && !frozen)
const persistCredentials = inputs.persistCredentials ?? true
const auth = !inputs.authHost
? undefined
: ((inputs.authToken
? {
host: inputs.authHost,
token: inputs.authToken
token: inputs.authToken,
persistCredentials: persistCredentials
}
: inputs.authCondaToken
? {
host: inputs.authHost,
condaToken: inputs.authCondaToken
condaToken: inputs.authCondaToken,
persistCredentials: persistCredentials
}
: inputs.authUsername
? {
host: inputs.authHost,
username: inputs.authUsername,
password: inputs.authPassword
password: inputs.authPassword,
persistCredentials: persistCredentials
}
: {
host: inputs.authHost,
s3AccessKeyId: inputs.authS3AccessKeyId,
s3SecretAccessKey: inputs.authS3SecretAccessKey,
s3SessionToken: inputs.authS3SessionToken
s3SessionToken: inputs.authS3SessionToken,
persistCredentials: persistCredentials
}) as Auth)
const postCleanup = inputs.postCleanup ?? true
const pypiKeyringProvider = inputs.pypiKeyringProvider
@@ -399,6 +410,7 @@ const inferOptions = (inputs: Inputs): Options => {
globalCache,
pixiBinPath,
auth,
persistCredentials,
postCleanup
}
}
@@ -447,6 +459,7 @@ const getOptions = () => {
authS3AccessKeyId: parseOrUndefined('auth-s3-access-key-id', z.string()),
authS3SecretAccessKey: parseOrUndefined('auth-s3-secret-access-key', z.string()),
authS3SessionToken: parseOrUndefined('auth-s3-session-token', z.string()),
persistCredentials: parseOrUndefinedJSON('persist-credentials', z.boolean()),
pypiKeyringProvider: parseOrUndefined('pypi-keyring-provider', pypiKeyringProviderSchema),
globalEnvironments: parseOrUndefinedMultilineList('global-environments', z.string()),
postCleanup: parseOrUndefinedJSON('post-cleanup', z.boolean())
@@ -477,3 +490,10 @@ try {
}
export const options = _options
export const assertAuth = () => {
const auth = options.auth
if (!auth) {
throw new Error('Authentication configuration is missing.')
}
return auth
}