Skip to main content

Scheduling updates

A workflow can simply install the CLI and run quant-ranger update, the same command described in Running updates.

Here we showcase a reusable workflow that wraps the quant-ranger CLI and how you can use it to run updaters. The examples come from the example plugin workspace: a repository whose Pixi environment provides quant-ranger and a plugin.

WorkflowResponsibility
One reusable workflowInstalls quant-ranger and builds the command line
One caller per updaterOwns that updater's schedule, defaults, and manual-dispatch inputs
warning

These are just starters, so adjust them to fit your setup.

The reusable workflow

This workflow takes an updater name and the global options as typed inputs, then assembles the argv for a single quant-ranger update call.

.github/workflows/updater.yml
name: Reusable Updater Workflow

on:
workflow_call:
inputs:
updater:
description: quant-ranger updater command to run.
required: true
type: string
updater-args:
description: Extra arguments passed after the updater command.
required: true
type: string
repositories:
description: Optional comma-separated repository filters passed to quant-ranger.
required: true
type: string
# Keep this as a string, otherwise we get a syntax error when forwarding the input from workflow_dispatch
# https://github.com/orgs/community/discussions/67182
jobs:
description: Number of quant-ranger update items to process concurrently.
required: true
type: string
runner:
description: GitHub runner label used for the updater job.
required: true
type: string
publish-changes:
description: Create or update pull requests instead of running in dry-run mode.
required: true
type: boolean
pr-details:
description: Show pull request details, including the full diff.
required: true
type: boolean
pr-details-diff-lines:
description: Trim pull request detail diffs to this many lines and enable PR details.
required: true
type: string
secrets:
GH_APP_PRIVATE_KEY:
description: GitHub App private key.
required: true

# quant-ranger authenticates as a GitHub App for the repositories it updates, so
# the workflow's own token only needs to read this repository.
permissions: {}

jobs:
run:
name: Run ${{ inputs.updater }} updater
runs-on: ${{ inputs.runner }}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Set up Pixi
uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0

- name: Allow rattler-sandbox user namespaces
run: |
# GitHub's Ubuntu AppArmor policy can deny the rootless uid_map write
# that rattler-sandbox uses; relax it in this ephemeral runner.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Run updater
env:
GH_APP_CLIENT_ID: ${{ vars.GH_APP_CLIENT_ID }}
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
REPOSITORIES: ${{ inputs.repositories }}
UPDATE_JOBS: ${{ inputs.jobs }}
PUBLISH_CHANGES: ${{ inputs.publish-changes }}
PR_DETAILS: ${{ inputs.pr-details }}
PR_DETAILS_DIFF_LINES: ${{ inputs.pr-details-diff-lines }}
UPDATER: ${{ inputs.updater }}
UPDATER_ARGS: ${{ inputs.updater-args }}
run: |
global_args=()
if [[ -n "$REPOSITORIES" ]]; then
global_args+=(--repository "$REPOSITORIES")
else
global_args+=(--all-installed-repositories)
fi

if [[ "$UPDATE_JOBS" != "1" ]]; then
global_args+=(--jobs "$UPDATE_JOBS")
fi
if [[ "$PUBLISH_CHANGES" == "true" ]]; then
global_args+=(--publish-changes)
fi
if [[ "$PR_DETAILS" == "true" ]]; then
global_args+=(--pr-details)
fi
if [[ -n "$PR_DETAILS_DIFF_LINES" ]]; then
global_args+=(--pr-details-diff-lines "$PR_DETAILS_DIFF_LINES")
fi

# Updater options are a single string here because `workflow_call`
# cannot pass a list; split it on whitespace into argv entries.
updater_args=()
if [[ -n "$UPDATER_ARGS" ]]; then
read -r -a updater_args <<< "$UPDATER_ARGS"
fi

pixi run quant-ranger update \
"${global_args[@]}" \
"$UPDATER" \
"${updater_args[@]}"

Without repositories, the workflow falls back to --all-installed-repositories, which processes every repository the app is installed on. See Select repositories.

Credentials

The workflow reads GitHub App credentials from one variable and one secret:

NameKindContents
GH_APP_CLIENT_IDRepository variableThe app's client ID (app ID)
GH_APP_PRIVATE_KEYRepository secretThe app's PEM private key

GitHub App lists the repository permissions the app needs, which differ between dry runs and publishing.

Loading plugins

quant-ranger discovers plugin updaters, aggregators, and site configs through the entry points of installed packages, so an installable plugin only has to be present in the same environment as the CLI. The easiest way to guarantee this is to define all your plugins inside a repository and then just depend on quant-ranger, like the example does.

Schedules for the pixi-update updater

pixi-update reads an autoupdate-schedule value from each repository's pixi.toml and can filter on it with --schedule. We have separate cron jobs for each of those schedules and select the schedule option of quant-ranger accordingly. This allows us to easily re-run those scheduled updaters manually if needed.

.github/workflows/updater-pixi-update.yml
name: Pixi Update Updater

on:
schedule:
# Weekly: Monday 08:23 UTC.
- cron: "23 8 * * 1"
# Monthly candidate: Monday 10:23 UTC; resolver keeps the first Monday.
- cron: "23 10 * * 1"
# Quarterly candidate: Monday 12:23 UTC; resolver keeps the first Monday.
- cron: "23 12 * 1,4,7,10 1"
workflow_dispatch:
inputs:
repositories:
description: Optional comma-separated repository filters passed to quant-ranger.
required: false
type: string
default: ""
jobs:
description: Number of quant-ranger update items to process concurrently.
required: false
type: string
default: "8"
runner:
description: GitHub runner label used for the updater job.
required: false
type: string
default: ubuntu-latest
publish-changes:
description: Create or update pull requests instead of running in dry-run mode.
required: false
type: boolean
default: false
pr-details:
description: Show pull request details, including the full diff.
required: false
type: boolean
default: false
pr-details-diff-lines:
description: Trim pull request detail diffs to this many lines and enable PR details.
required: false
type: string
default: ""
schedule:
description: Optional schedule filter for manual Pixi update runs.
required: false
type: choice
default: no-filter
options:
- no-filter
- weekly
- monthly
- quarterly

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
# GitHub reports which cron fired, but not what it means. Map it to a
# `--schedule` value in an unprivileged job so the updater job stays simple.
resolve-schedule:
name: Resolve schedule
runs-on: ubuntu-latest
permissions: {}
outputs:
updater-args: ${{ steps.schedule.outputs.updater-args }}
run-update: ${{ steps.schedule.outputs.run-update }}
steps:
- name: Resolve schedule
id: schedule
env:
CRON: ${{ github.event.schedule }}
REQUESTED_SCHEDULE: ${{ github.event_name == 'workflow_dispatch' && inputs.schedule || '' }}
run: |
if [[ "$REQUESTED_SCHEDULE" == "no-filter" ]]; then
schedule=
elif [[ -n "$REQUESTED_SCHEDULE" ]]; then
schedule="$REQUESTED_SCHEDULE"
else
run_update=true
case "$CRON" in
"23 8 * * 1") schedule=weekly ;;
"23 10 * * 1") schedule=monthly ;;
"23 12 * 1,4,7,10 1") schedule=quarterly ;;
*)
echo "Unknown Pixi update schedule cron: $CRON" >&2
exit 1
;;
esac
# Cron cannot express "first Monday of the month", so the monthly and
# quarterly crons fire every Monday and are dropped outside the first
# week. `10#` forces base 10 so a leading zero is not read as octal.
if [[ "$schedule" != "weekly" ]] && (( 10#$(date -u +%d) > 7 )); then
run_update=false
fi
fi
run_update=${run_update:-true}
updater_args=
if [[ -n "$schedule" ]]; then
updater_args="--schedule $schedule"
fi
echo "updater-args=$updater_args" >> "$GITHUB_OUTPUT"
echo "run-update=$run_update" >> "$GITHUB_OUTPUT"

update:
name: Run pixi update updater
needs: resolve-schedule
if: ${{ needs.resolve-schedule.outputs.run-update == 'true' }}
permissions:
contents: read
uses: ./.github/workflows/updater.yml
with:
updater: pixi-update
updater-args: ${{ needs.resolve-schedule.outputs.updater-args }}
repositories: ${{ inputs.repositories }}
jobs: ${{ inputs.jobs || '8' }}
runner: ${{ inputs.runner || 'ubuntu-latest' }}
# Scheduled runs publish; manual runs are dry runs unless asked otherwise.
publish-changes: ${{ github.event_name == 'schedule' || inputs.publish-changes }}
pr-details: ${{ inputs.pr-details || false }}
pr-details-diff-lines: ${{ inputs.pr-details-diff-lines }}
secrets:
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}

The resolve-schedule job maps the cron string to a --schedule value and decides whether to run at all. This is necessary because we set the monthly/quaterly schedules to run on the same weekday. Otherwise, they might randomly overlap and drain the GitHub API rate limit.

See Schedule filtering for how --schedule compares against each repository's configuration.

Schedules for other updaters

Most updaters have no schedule filter at the moment. In this case we can just forward useful inputs to manual dispatch inputs and set up a schedule. Here an example using the copier updater:

.github/workflows/updater-copier.yml
name: Copier Updater

on:
schedule:
# This is Monday 00:23 UTC.
- cron: "23 0 * * 1"
workflow_dispatch:
inputs:
repositories:
description: Optional comma-separated repository filters passed to quant-ranger.
required: false
type: string
default: ""
jobs:
description: Number of quant-ranger update items to process concurrently.
required: false
type: string
default: "1"
runner:
description: GitHub runner label used for the updater job.
required: false
type: string
default: ubuntu-latest
publish-changes:
description: Create or update pull requests instead of running in dry-run mode.
required: false
type: boolean
default: false
pr-details:
description: Show pull request details, including the full diff.
required: false
type: boolean
default: false
pr-details-diff-lines:
description: Trim pull request detail diffs to this many lines and enable PR details.
required: false
type: string
default: ""

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
update:
name: Run copier updater
permissions:
contents: read
uses: ./.github/workflows/updater.yml
with:
updater: copier
# The copier updater takes no options of its own.
updater-args: ""
repositories: ${{ inputs.repositories }}
jobs: ${{ inputs.jobs || '1' }}
runner: ${{ inputs.runner || 'ubuntu-latest' }}
# Scheduled runs publish; manual runs are dry runs unless asked otherwise.
publish-changes: ${{ github.event_name == 'schedule' || inputs.publish-changes }}
pr-details: ${{ inputs.pr-details || false }}
pr-details-diff-lines: ${{ inputs.pr-details-diff-lines }}
secrets:
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}

This is the same shape as the pixi-update caller minus the resolve-schedule job, with updater-args: "" because the copier updater takes no options of its own. Add one such file per updater you want to run.

Processing results

To add further steps, add --results-file and pass the file to an aggregator. See Results and aggregation and the built-in aggregators.