Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
479 changes: 479 additions & 0 deletions .github/scripts/monitor_nightly.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .github/scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Dependencies for monitor_nightly.py (used by monitor-nightly.yml).
PyGithub==2.9.1
PyYAML==6.0.2
requests==2.32.5
37 changes: 37 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Lint scripts

# Lints and format-checks the Python helpers under scripts/ and .github/scripts/
# that are run by the action and the monitor workflow.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

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

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0

# ruff is pinned inline so a new release cannot change the lint outcome
# unexpectedly (the rule selection is also pinned in ruff.toml). Bump
# deliberately.
- name: Lint (ruff check)
run: uvx ruff@0.16.0 check scripts .github/scripts

- name: Format check (ruff format)
run: uvx ruff@0.16.0 format --check scripts .github/scripts
70 changes: 70 additions & 0 deletions .github/workflows/monitor-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Monitor nightly channel

# Watches the scientific-python-nightly-wheels channel and files issues on this
# repository when a package stops receiving uploads:
# * > 30 days without an upload -> open a "stale" issue (auto-closed on recovery)
# * > 60 days without an upload -> additionally open a "purge candidate" issue
# asking maintainers to remove the package from the nightly channel.
#
# This complements remove-wheels.yml (which prunes old *versions*); here we only
# open/close tracking issues and never delete anything. Packages listed in
# packages-ignore-from-cleanup.txt are intentionally exempt and are skipped.
#
# The logic lives in .github/scripts/monitor_nightly.py (dependencies pinned in
# .github/scripts/requirements.txt).

on:
schedule:
# Every day at 07:00 UTC, after the nightly uploads have had time to land.
- cron: '0 7 * * *'
workflow_dispatch:

# Least-privilege GITHUB_TOKEN. This job only checks out the repo (to read the
# monitor script and the ignore list) and opens/closes/comments on issues;
# package data comes from the public anaconda.org API. Any scope omitted from a
# `permissions:` block already defaults to `none`; the `none` entries below are
# spelled out for readability so the intent is explicit.
permissions:
issues: write # create / comment on / close the tracking issues
contents: read # checkout: read the script + packages-ignore-from-cleanup.txt
actions: none
attestations: none
checks: none
deployments: none
discussions: none
id-token: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none

concurrency:
group: monitor-nightly
cancel-in-progress: false

jobs:
monitor:
runs-on: ubuntu-latest
if: github.repository_owner == 'scientific-python'
steps:
- name: Check out the action
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.x'

- name: Install dependencies
run: python -m pip install -r "${GITHUB_WORKSPACE}/.github/scripts/requirements.txt"

- name: Check nightly upload freshness
env:
GITHUB_TOKEN: ${{ github.token }}
# Optional PAT with `issues: write` on the producing repos. When set
# (together with entries in packages-source-repos.yaml), the monitor
# also opens a tracking issue on each stalled wheel's own repository.
PRODUCER_GITHUB_TOKEN: ${{ secrets.NIGHTLY_UPLOAD_ISSUE_PAT }}
run: python3 "${GITHUB_WORKSPACE}/.github/scripts/monitor_nightly.py"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# pixi environments
.pixi
*.egg-info

# python
__pycache__/
.ruff_cache/
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,43 @@ updates:
interval: "weekly"
```

## Getting notified when an upload fails

If a nightly upload silently starts failing, downstream projects can go weeks
without fresh wheels. To catch this, set `report_failures: true`. When the
upload fails the action opens (or reuses) an issue on the repository running the
action, and closes it automatically on the next successful upload.

This uses the automatically-provided `github.token`, so no extra secret is
needed — but the calling workflow must grant `issues: write`:

```yml
jobs:
upload:
permissions:
# `report_failures` needs `issues: write`; that is the only scope the
# action itself requires. Any scope you do not list defaults to `none`.
# `contents: read` is only needed if the job also checks out the repo.
issues: write
contents: read
steps:
...
- name: Upload wheel
uses: scientific-python/upload-nightly-action@main
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.UPLOAD_TOKEN }}
report_failures: true
```

Additional inputs:

| Input | Default | Description |
| --- | --- | --- |
| `report_failures` | `false` | Open/close a tracking issue on the calling repo when the upload fails/recovers. |
| `github_token` | `${{ github.token }}` | Token used to manage the tracking issue. Override to open the issue on another repo. |
| `issue_repository` | current repo | `owner/name` where the tracking issue should be opened. |

## Access to the ``scientific-python-nightly-wheels`` channel

To request access to the wheel channel, please open an issue on [the upload action's
Expand Down Expand Up @@ -77,6 +114,46 @@ Any versions beyond these are automatically removed as part of a daily cron job
Projects may have reasons to request to be added to the list exempt from this automated cleanup, however
in that case the responsibility of cleaning-up old, unused versions fall back on the individual project.

## Monitoring channel freshness (maintainers)

In addition to pruning old *versions* (see above), a scheduled workflow
([`.github/workflows/monitor-nightly.yml`](.github/workflows/monitor-nightly.yml))
watches for packages that have stopped receiving uploads entirely and files
issues on this repository:

- **> 30 days** without an upload → opens a `stale-nightly` issue for the package
(automatically closed once a fresh upload lands).
- **> 60 days** without an upload → additionally opens a `nightly-purge-candidate`
issue asking maintainers to decide whether to purge the package from the
channel (also auto-closed on recovery).

It never deletes anything, runs a small Python script
([`.github/scripts/monitor_nightly.py`](.github/scripts/monitor_nightly.py), using
PyGithub) with the built-in `github.token`, and skips packages listed in
[`packages-ignore-from-cleanup.txt`](packages-ignore-from-cleanup.txt) so that
intentionally-exempt packages are not flagged.

### Notifying the projects directly (optional)

The monitor can also open a tracking issue on **each wheel's own repository** so
the people who can fix the build hear about it early:

- **> 15 days** without an upload → opens a `nightly-upload-stalled` issue on the
producing repo, escalated with a comment at the 30- and 60-day marks and
auto-closed on recovery.

Because the default `github.token` cannot write issues on other repositories,
this requires a Personal Access Token with `issues: write` on those repos, stored
as the `NIGHTLY_UPLOAD_ISSUE_PAT` secret, plus a hand-maintained wheel → repo
mapping in
[`packages-source-repos.yaml`](packages-source-repos.yaml). It is fully opt-in:
with no PAT (or an empty mapping) this behaviour is skipped entirely.

If the PAT is set but invalid, expired, or unauthorized, the monitor opens a
`nightly-pat-invalid` issue on *this* repository (using the built-in token) so
maintainers know to rotate the secret, and closes it automatically once the token
works again.

# Using nightly builds in CI

To test against nightly builds, you can use the following command to install from
Expand Down
46 changes: 46 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ inputs:
description: 'List of labels assigned to the uploaded artifacts'
required: false
default: main
report_failures:
description: >-
When "true", open an issue on the repository running this action if the
upload fails, and close it automatically on the next successful upload.
The calling workflow must grant `permissions: issues: write`.
required: false
default: 'false'
github_token:
description: >-
Token used to open/close the failure-reporting issue. Defaults to the
automatically provided `github.token`; override only if you need to open
the issue on a different repository with a token that has access to it.
required: false
default: ${{ github.token }}
issue_repository:
description: >-
Repository (in "owner/name" form) where the failure issue should be opened.
Defaults to the repository running the action.
required: false
default: ''

runs:
using: "composite"
Expand All @@ -34,11 +54,37 @@ runs:
manifest-path: ${{ github.action_path }}/pixi.toml

- name: Upload wheels
id: upload
shell: bash
# When failure reporting is enabled we must not abort here, so that the
# reporting step below can run. The final step re-raises the failure.
continue-on-error: ${{ inputs.report_failures == 'true' }}
env:
INPUT_ARTIFACTS_PATH: ${{ inputs.artifacts_path }}
INPUT_ANACONDA_NIGHTLY_UPLOAD_ORGANIZATION: ${{ inputs.anaconda_nightly_upload_organization }}
INPUT_ANACONDA_NIGHTLY_UPLOAD_TOKEN: ${{ inputs.anaconda_nightly_upload_token }}
INPUT_ANACONDA_NIGHTLY_UPLOAD_LABELS: ${{ inputs.anaconda_nightly_upload_labels }}
run: |
pixi run --manifest-path ${{ github.action_path }}/pixi.toml ${{ github.action_path }}/upload_wheels.sh

- name: Report upload status
if: ${{ inputs.report_failures == 'true' }}
shell: bash
# Issue reporting is best-effort: on pull requests / forks the token is
# read-only and cannot open issues. Never fail the build over reporting;
# a genuine upload failure is still surfaced by the next step.
continue-on-error: true
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
UPLOAD_OUTCOME: ${{ steps.upload.outcome }}
ISSUE_REPOSITORY: ${{ inputs.issue_repository }}
run: |
pixi run --manifest-path ${{ github.action_path }}/pixi.toml \
python ${{ github.action_path }}/scripts/report_failure.py

- name: Propagate upload failure
if: ${{ steps.upload.outcome == 'failure' }}
shell: bash
run: |
echo "::error::Nightly wheel upload failed." >&2
exit 1
31 changes: 31 additions & 0 deletions packages-source-repos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Hand-maintained mapping: nightly wheel name -> source GitHub repository.
#
# Used by .github/scripts/monitor_nightly.py to open a tracking issue on a
# project's OWN repository when its nightly wheels stop being uploaded (see the
# "Monitoring channel freshness" section of the README).
#
# This only takes effect when the monitor workflow is given a Personal Access
# Token via the secrets.NIGHTLY_UPLOAD_ISSUE_PAT secret (exposed to the script
# as PRODUCER_GITHUB_TOKEN) that has `issues: write` on the listed repositories.
# Without that token this file is ignored.
#
# Each entry maps the wheel name (as published on
# https://anaconda.org/scientific-python-nightly-wheels ) to its source repo.
# An entry may be a bare "owner/repo" string, or a mapping with a `repo:` key so
# that per-repo configuration (e.g. custom labels or assignees) can be added
# later without changing the file format.

packages:
# Dummy entry, kept so the file is always valid YAML and the schema is
# exercised in CI. It is inert in practice: no wheel named
# "example-nightly-package" exists on the channel, so it never triggers.
# Replace the entries below with real projects.
example-nightly-package:
repo: scientific-python/upload-nightly-action

# Real examples (uncomment and verify before enabling):
# contourpy: contourpy/contourpy
# scikit-image:
# repo: scikit-image/scikit-image
# sunpy:
# repo: sunpy/sunpy
6 changes: 6 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Pin the rule selection so `ruff check` stays stable across ruff releases
# (ruff's default rule set changes between versions). Change this deliberately.
line-length = 88

[lint]
select = ["E4", "E7", "E9", "F", "W", "I"]
Loading
Loading