| title | Workflows Reference |
|---|---|
| layout | default |
| nav_order | 8 |
This page describes the complete lifecycle of a package in python-wheels: the
first contribution, automated upgrades, publication, documentation, and eventual
deprecation. For build implementation details, see the development
guide.
A package normally moves through these states:
- A contributor opens a PR containing
.github/workflows/build-<pkg>.ymlanddocs/packages/<pkg>.yaml. The YAML contains a pending version. - The PR builds and tests that version. Its
publishjob calls_publish-wheel.ymlin dry-run mode; no release or automated PR is created. - Merging the PR triggers the package workflow on
main. The wheels are built again and published in a new immutable GitHub Release. - The publish job pushes the release metadata to
package/<pkg>and opens or updates a PR titled<pkg>: update packages. - Merging that package PR makes the release visible in the generated Simple API and triggers deployment of the website.
- Each night,
check_versionscompares tracked packages with PyPI. An upgrade, yanked release, or deprecation is proposed on the samepackage/<pkg>branch and in a PR with the same<pkg>: update packagestitle. - Merging an upgrade PR returns to step 3. A deprecation PR instead retires the package when PyPI provides an upstream riscv64 or universal wheel.
The central design rule is that all automated changes for one package share
package/<pkg>, while different packages use different branches. Publication
and nightly version checks are also serialized by the publish-wheel
concurrency group. This prevents the two automation sources from overwriting one
another without forcing unrelated package changes into one branch or PR.
A new package contribution contains at least:
.github/workflows/build-<pkg>.yml;docs/packages/<pkg>.yaml.
It may also contain versioned patches under patches/<pkg>/<version>/. A patch
must contain a valid Upstream-Status: header. The package YAML itself registers
the package for publication and maintenance.
The package YAML is the source of truth for versions:
package-name: <pkg>
source-code: <upstream repository URL>
license: <SPDX identifier>
versions:
- version: <wheel version>The version is the value that will appear in the wheel's METADATA, which is
not necessarily identical to the upstream git tag. An entry with neither tag
nor files is pending. A released entry has both keys. A partial entry with
only one of them is invalid.
Every build-<pkg>.yml must have all three triggers, with both the workflow and
package YAML in the pull_request and push path filters:
on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/<pkg>.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-<pkg>.yml'
- 'docs/packages/<pkg>.yaml'
push:
branches: [main]
paths:
- '.github/workflows/build-<pkg>.yml'
- 'docs/packages/<pkg>.yaml'They have distinct purposes:
pull_requestregisters a brand-new workflow with GitHub and validates the pending versions before merge;pushperforms the post-merge build and publication frommain;workflow_dispatchis for an intentional rebuild, selected with a version glob. It is not the normal post-merge mechanism.
A new workflow cannot be usefully dispatched before GitHub has registered it.
The first PR run provides that registration. Do not remove the PR trigger to
avoid a duplicate build: the PR run validates, while the main run publishes.
The first job calls /.github/workflows/_setup.yml:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: <pkg>
version: ${{ inputs.version }}_setup.yml runs ci_scripts/pending_versions.py and returns a JSON version
list:
- without a dispatch glob, every pending version is returned, except that on a
pull request from a
package/<pkg>branch only the versions the PR adds todocs/packages/<pkg>.yamlon top of the base branch are returned, so linking a release does not rebuild versions already merged but not yet released; - with a dispatch glob, every matching declared version is returned, including versions already released;
- if nothing is pending, it returns
[].
Every matrix job must guard against the empty list with
if: needs.setup.outputs.versions != '[]'. Build and test jobs consume the
selected versions as matrix.version, create wheel artifacts, and should mirror
upstream's Linux build and tests as closely as possible. The standard target
matrix and implementation patterns are documented in Development
guidelines.
Every package workflow ends with a matrix job that calls the reusable publish workflow:
publish:
name: Publish <pkg> ${{ matrix.version }}
needs: [setup, build_wheels]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: <pkg>-${{ matrix.version }}-*-manylinux_riscv64
secrets:
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}The artifact pattern must select wheels for exactly one normalized package and
one version. If GPL source correspondence is required, the caller also supplies
gpl-sources-artifact and gpl-sources-description; the source tarball is then
published beside the wheels and recorded in the package YAML.
The PR's package workflow builds and tests every pending version. The publish
job downloads and inspects the selected wheels, verifies that their normalized
project names and versions agree, computes the prospective release tag, and
prints a dry-run summary. ci_scripts/update_doc.py also prints the YAML diff
and branch/PR it would create.
Because the ref is not main, a PR run does not create a GitHub Release,
push package/<pkg>, or open a package PR. Reviewers should verify:
- the workflow follows upstream's build and test process;
- all expected Python/platform wheels exist and no unrelated artifact matched;
- wheel metadata contains the declared package name and version;
- tests exercised the installed wheel rather than the source checkout;
- package and third-party licenses are present;
- corresponding GPL sources are configured when required;
- the publish dry run shows the expected release and files;
pr-checks.ymlaccepts the package YAML, workflow association, commits, and any patch headers.
Merging the first contribution or an upgrade PR places a pending version on
main. The package workflow's push trigger builds and tests it again. This
second build is intentional: only reviewed code from main may publish.
For each selected version, _publish-wheel.yml:
- downloads the matching artifacts and rejects an empty or mixed package/version set;
- creates a draft GitHub Release targeting the triggering commit;
- uses release title
<normalized-package>-v<version>and tag<normalized-package>-v<version>-<UTC timestamp>; - uploads all wheels and, when configured,
gpl-sources.tar; - publishes the release and verifies that repository immutable releases froze it;
- runs
ci_scripts/update_doc.pyto record the publication.
Every successful main publication creates a new immutable release. A manual
rebuild therefore creates a new timestamped release rather than editing an old
one. Publication fails if no wheels match, wheels contain mixed names or
versions, the wheel version is undeclared, GPL sources are missing, or the
resulting release is not immutable.
A rebuild leaves the previous release behind: the YAML entry moves to the new
tag, so the old release is linked from nowhere. gc-releases.yml (manual
dispatch, dry run by default) runs ci_scripts/gc-releases.py, which deletes
every release named <package>-v<version>-<timestamp> whose tag is not the
tag of a version entry in docs/packages/*.yaml on main. A tag recorded by
the YAML of an open package/<pkg> pull request is kept, because merging that
PR links it.
The script refuses to run if it finds no tag at all or would delete more than
half of the releases. It shares the publish-wheel concurrency group so no
publication is in flight, and the git tag of a deleted release stays, as an
immutable release's tag cannot be removed.
update_doc.py normalizes the wheel's project name and uses:
- branch:
package/<normalized-pkg>; - PR title:
<normalized-pkg>: update packages; - base:
main.
If the branch already exists, it is fetched and rebased onto main before the
new commit is made. The push uses --force-with-lease, so an unexpected branch
update is rejected rather than overwritten. If an open PR already uses the
branch, publication pushes to it instead of opening another PR.
The publication commit fills the declared version entry with:
- the immutable release
tag; - every wheel's
filenameand SHA-256 digest; requires-pythonwhen present in wheel metadata;- GPL source asset metadata when configured;
patched: truewhen a matching patch directory exists;- a per-version license only when wheel metadata identifies a meaningful change from the package's top-level license.
On a package's first publication, the package YAML makes it eligible for nightly checks without a separate package list.
The publish job in _publish-wheel.yml and the check_versions job in
nightly.yml use the same repository-wide concurrency group:
concurrency:
group: publish-wheel
cancel-in-progress: false
queue: maxOnly one of these jobs mutates package branches at a time. queue: max retains
waiting jobs instead of replacing an older pending job, and
cancel-in-progress: false lets the active publication or nightly check finish.
This is deliberately job-level concurrency: package builds may proceed in
parallel, but branch/PR mutation is serialized.
The automated package PR may contain a nightly declaration, publication metadata, or both. Review the complete diff rather than assuming it came from only one source.
For publication metadata, verify:
- top-level
licenseis a short valid identifier (preferably SPDX), not a file path or full license text; - every expected wheel is listed exactly once with the correct filename,
SHA-256 digest, and
requires-pythonvalue; - the tag names the expected immutable GitHub Release and its assets match the YAML;
gpl-sourcesis present when the wheel requires corresponding GPL sources;patched: trueand anycomment:orwarning:accurately describe carried changes;- no unrelated package files were changed.
Merging <pkg>: update packages places tag and files on main, so the
version is no longer pending. That push has two effects:
- the package build workflow may be triggered by its YAML path, but
_setup.ymlreturns[], so all guarded build/publish jobs skip; website.ymlregenerates package pages and the static PEP 503/691 Simple API, builds the Jekyll site, and deploys it topypi.riseproject.dev.
The GitHub Release exists before this merge, but pip cannot discover it through
the RISE index until the YAML reaches main and the website deployment
completes. Publication is therefore not complete until the package PR is merged
and the site deploy succeeds.
nightly.yml runs on a schedule (Mondays and Thursdays at 02:00 UTC), by manual dispatch, and for PRs that modify
its version-checking logic. Its check_versions job lists active package YAML
files under docs/packages/ and compares their release state with PyPI.
The detection phase leaves the checkout clean and writes reports. For each package it can detect:
- stable PyPI releases not yet declared in the package YAML;
- released versions that PyPI has yanked;
- a point where upstream starts shipping a riscv64 wheel;
- a point where upstream switches to a universal pure-Python wheel, making a separate RISE binary build unnecessary;
- malformed/missing metadata or network/API errors.
A release already supplied upstream as riscv64 or pure Python is not added as a new RISE build. Deprecation waits until all already-declared versions have been published, so a pending build is not silently abandoned.
When --declare adds a version of a package, it copies the patch directory of
the version declared immediately before it to patches/<pkg>/<new-version>/
(keeping a local suffix such as +cpu); several new versions chain in order.
If that preceding version has no patches, none are copied, since they may have
been upstreamed. The build shows whether the copied patches still apply.
For every actionable result, the workflow re-runs
check_versions.py --declare <package> in isolation. It uses the same
package/<normalized-pkg> branch as publication:
- fetch the existing package branch when present, otherwise start at
main; - rebase existing package work onto the current
mainand push that rebase even if the requested declaration is already present; - declare new pending versions, mark yanked versions, or apply deprecation;
- commit only
docs/packages/<pkg>.yamland any carried patches; - push with a lease when updating an existing branch;
- open or update a draft PR titled
<normalized-pkg>: update packages.
The PR body explains the detected upgrade/deprecation/yank. For an upgrade, the
new bare - version: entries trigger that package's build-<pkg>.yml PR run.
The build and publish dry run must pass before merge. Merging the nightly PR
then invokes the same main publication path described above.
Using the existing package branch is important when nightly and publication cross paths. For example, if publication metadata is awaiting review and a new upstream version appears, nightly rebases and appends the declaration instead of replacing the metadata. Conversely, publication can fill a version already declared by nightly on that branch. Shared concurrency prevents both jobs from performing those operations simultaneously.
When upstream supplies riscv64 wheels (including a compatible abi3 wheel) or
only universal pure-Python wheels, nightly can propose deprecation. The package
YAML is marked with deprecated:. check_deprecated_packages discovers those
YAML files directly and continues checking that deprecated upstream support has
not disappeared.
Versions yanked on PyPI are marked yanked: true in the package YAML. The
Simple API renders this using the corresponding package-index yank metadata.
A package PR can contain a yank together with an upgrade or deprecation.
The other nightly jobs are independent of package-branch mutation:
check_deprecated_packagesverifies that deprecated packages still provide a riscv64 or universal wheel upstream;pip_auditscans the installed packages and reports vulnerabilities.
Use workflow_dispatch only when intentionally rebuilding a declared version:
gh workflow run build-<pkg>.yml --ref main -f version='<glob>'A glob can select released versions as well as pending ones. On main, each
match creates a new immutable release and updates its YAML entry to the newest
release. On a non-main ref, publication remains a dry run.
Important failure boundaries:
- If build or tests fail, no release is created.
- If release creation/upload succeeds but documentation update fails, the immutable release may already exist; rerunning creates another release.
- A force-with-lease failure means the package branch changed unexpectedly. Re-run after inspecting the branch; do not replace it with an unconditional force-push.
- If the package PR is not merged, the release remains undiscoverable in the
RISE Simple API and the YAML version remains pending on
main. - Do not dispatch a rebuild merely because the normal post-merge
pushrun is still queued or running.
GitHub restricts recursive workflow triggering from the repository's default
GITHUB_TOKEN; depending on the event, runs are suppressed or require manual
approval. nightly.yml and _publish-wheel.yml therefore prefer an installation
token for the riseproject-dev GitHub App so package PR checks start normally.
They fall back to GITHUB_TOKEN when App credentials are unavailable, including
untrusted pull-request contexts.
Required repository configuration:
- variable
RISEPROJECT_APP_CLIENT_ID; - secret
RISEPROJECT_APP_PRIVATE_KEY; - App installation access to the repository, with write access to contents and pull requests and read access to issues.
Because _publish-wheel.yml is reusable, each package workflow must explicitly
pass the private key as app-private-key. Token permissions can only be
maintained or reduced across a reusable-workflow call, so the caller's publish
job must grant contents: write and pull-requests: write.
_setup.ymlselects pending or explicitly requested package versions._publish-wheel.ymlinspects artifacts, creates immutable releases, and updates package branches.nightly.ymldetects upgrades, yanks, deprecations, installation failures, and known vulnerabilities.pr-checks.ymlchecks commit/PR policy, validates package YAML/workflow pairing, and validates patchUpstream-Statusheaders.website.ymlturnsdocs/packages/*.yamlinto package documentation and the Simple API, then deploys GitHub Pages.
RISE maintains the [RISC-V Wheels]({{ '/dashboard/' | relative_url }}) dashboard,
which tracks riscv64 compatibility across the most-downloaded binary Python
wheels, including wheels available in the RISE index but not upstream. Consult
it before deciding whether a package still needs a python-wheels build.
The dashboard is regenerated during website deployment. Its generator is
vendored in ci_scripts/dashboard/ from
riseproject-dev/python-wheels-dashboard,
which remains its upstream source of truth.