Publish Ruff crates to crates.io (#26271)

## Summary

Publish Ruff's production Rust crates to crates.io as part of the
existing release workflow. This follows uv's setup: workspace
dependencies carry registry-compatible version pins, internal crates
receive generated READMEs and an explicit versioning policy, CI dry-runs
the full workspace publish, and release jobs publish missing crate
versions idempotently through crates.io trusted publishing.

The publishing set contains 32 packages: Ruff's CLI, WASM package,
server, linter, formatter, parser, and the internal Ruff component
crates required by them. It also includes `ty_module_resolver`,
`ty_site_packages`, and `ty_static`, because those three crates are part
of Ruff's normal dependency closure.

The publishing set deliberately excludes:

- Test-only crates: `mdtest`, `ruff_mdtest`,
`ruff_python_ast_integration_tests`, and
`ruff_python_trivia_integration_tests`.
- Benchmark and development crates: `ruff_benchmark` and `ruff_dev`.
- All remaining `ty_*` crates.

These packages retain `publish = false`, so bootstrap, version bumping,
README generation, CI dry-runs, and release publishing skip them through
Cargo metadata instead of a separate hard-coded allowlist.

The crates.io job is intentionally independent of release announcement,
so a crates.io outage cannot block the existing binary, PyPI, npm, or
GitHub release outputs. A bootstrap script reserves new crate names,
configures the release workflow as each crate's trusted publisher,
enables trusted-publishing-only releases, and records configured crates
for idempotent reruns.

The full workspace publish dry-run packages and verifies all 32 selected
crates.

Closes https://github.com/astral-sh/ruff/issues/43.
This commit is contained in:
Charlie Marsh authored and GitHub committed 2026-06-23 12:49:23 -04:00
1 parent a4bfeb12f9
commit dff016b7a4
81 files changed
+1705 -124

No files matched your search

+16
View File
@@ -301,6 +301,22 @@ jobs:
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm -p ty_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
cargo-publish-dry-run:
name: "cargo publish dry-run"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "cargo publish dry-run"
run: cargo publish --workspace --dry-run
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
+38
View File
@@ -0,0 +1,38 @@
# Publish a release to crates.io.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job
# within `cargo-dist`.
name: "Publish to crates.io"
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
crates-publish-ruff:
name: Upload Ruff to crates.io
runs-on: ubuntu-latest
environment:
name: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
id: auth
- name: Install nightly toolchain
# Nightly is required for the unstable `-Zpublish-timeout` flag, which lets us
# raise the per-crate wait above the 60s default. crates.io indexing has been
# known to lag long enough to exceed that during workspace publishes.
run: rustup toolchain install nightly-2026-06-12 --profile minimal --no-self-update
- name: Publish workspace crates
# Note `--no-verify` is safe because we do a publish dry-run elsewhere in CI
run: python3 scripts/publish-crates.py --cargo cargo +nightly-2026-06-12 --no-verify -- -Zpublish-timeout --config 'publish.timeout=600'
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
+19
View File
@@ -273,6 +273,23 @@ jobs:
"id-token": "write"
"packages": "write"
custom-publish-crates:
needs:
- plan
- host
- release-gate
- custom-publish-pypi # DIRTY: see #16989
- custom-publish-wasm # DIRTY: see #16989
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-crates.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"contents": "read"
"id-token": "write"
# Create a GitHub Release while uploading all files to it
announce:
needs:
@@ -284,6 +301,8 @@ jobs:
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
# `custom-publish-crates` is intentionally not a dependency because a crates.io outage should
# not block the rest of the release.
if: ${{ always() && needs.host.result == 'success' && needs.release-gate.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-wasm.result == 'skipped' || needs.custom-publish-wasm.result == 'success') }}
runs-on: "depot-ubuntu-latest-4"
permissions:
+1
View File
@@ -0,0 +1 @@
# GENERATED-BY scripts/setup-crates-io-publish.py
+28
View File
@@ -171,6 +171,34 @@ At the time of writing, the repository includes the following crates:
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the
[Ruff Playground](https://play.ruff.rs/).
#### Adding a new crate
When adding a workspace crate under `crates/`, first decide whether it should be published to
crates.io as part of Ruff's releases:
- Test, benchmark, development, and other non-release crates must set `publish = false` in their
`Cargo.toml`.
- Publishable crates should inherit the workspace package metadata and follow Ruff's [crate
versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning). If the crate is
listed under `[workspace.dependencies]`, specify both its path and matching version.
For a publishable crate, generate its README and verify that the workspace can still be packaged:
```shell
uv run --script scripts/generate-crate-readmes.py
cargo publish --workspace --dry-run
```
Before merging a publishable crate, ask a crates.io owner to bootstrap it by running:
```shell
CARGO_REGISTRY_TOKEN=<token> uv run --no-config --script scripts/setup-crates-io-publish.py
```
The bootstrap script reserves the crate name, configures the release workflow as its trusted
publisher, requires trusted publishing for future versions, and adds the crate to `.known-crates`.
Commit the generated README and `.known-crates` update with the new crate.
### Example: Adding a new lint rule
At a high level, the steps involved in adding a new lint rule are as follows:
Generated
+28 -28
View File
@@ -3029,7 +3029,7 @@ dependencies = [
[[package]]
name = "ruff_annotate_snippets"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"anstream 1.0.0",
"anstyle",
@@ -3069,7 +3069,7 @@ dependencies = [
[[package]]
name = "ruff_cache"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"filetime",
"glob",
@@ -3082,7 +3082,7 @@ dependencies = [
[[package]]
name = "ruff_db"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anstyle",
"arc-swap",
@@ -3173,7 +3173,7 @@ dependencies = [
[[package]]
name = "ruff_diagnostics"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"get-size2",
"is-macro",
@@ -3183,7 +3183,7 @@ dependencies = [
[[package]]
name = "ruff_formatter"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"drop_bomb",
"ruff_cache",
@@ -3199,7 +3199,7 @@ dependencies = [
[[package]]
name = "ruff_graph"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"anyhow",
"clap",
@@ -3220,7 +3220,7 @@ dependencies = [
[[package]]
name = "ruff_index"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"get-size2",
"ruff_macros",
@@ -3292,7 +3292,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"heck",
"itertools 0.14.0",
@@ -3305,7 +3305,7 @@ dependencies = [
[[package]]
name = "ruff_markdown"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"insta",
"regex",
@@ -3335,14 +3335,14 @@ dependencies = [
[[package]]
name = "ruff_memory_usage"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"get-size2",
]
[[package]]
name = "ruff_notebook"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -3360,14 +3360,14 @@ dependencies = [
[[package]]
name = "ruff_options_metadata"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"serde",
]
[[package]]
name = "ruff_python_ast"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"aho-corasick",
"arrayvec",
@@ -3403,7 +3403,7 @@ dependencies = [
[[package]]
name = "ruff_python_codegen"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"ruff_python_ast",
"ruff_python_literal",
@@ -3415,7 +3415,7 @@ dependencies = [
[[package]]
name = "ruff_python_formatter"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"clap",
@@ -3448,7 +3448,7 @@ dependencies = [
[[package]]
name = "ruff_python_importer"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"insta",
@@ -3463,7 +3463,7 @@ dependencies = [
[[package]]
name = "ruff_python_index"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"ruff_python_ast",
"ruff_python_parser",
@@ -3474,7 +3474,7 @@ dependencies = [
[[package]]
name = "ruff_python_literal"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"bitflags 2.13.0",
"icu_properties",
@@ -3484,7 +3484,7 @@ dependencies = [
[[package]]
name = "ruff_python_parser"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"bitflags 2.13.0",
@@ -3513,7 +3513,7 @@ dependencies = [
[[package]]
name = "ruff_python_semantic"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"bitflags 2.13.0",
"insta",
@@ -3534,7 +3534,7 @@ dependencies = [
[[package]]
name = "ruff_python_stdlib"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"bitflags 2.13.0",
"unicode-ident",
@@ -3542,7 +3542,7 @@ dependencies = [
[[package]]
name = "ruff_python_trivia"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"itertools 0.14.0",
"ruff_source_file",
@@ -3562,7 +3562,7 @@ dependencies = [
[[package]]
name = "ruff_server"
version = "0.2.2"
version = "0.0.1"
dependencies = [
"anyhow",
"crossbeam",
@@ -3604,7 +3604,7 @@ dependencies = [
[[package]]
name = "ruff_source_file"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"get-size2",
"memchr",
@@ -3614,7 +3614,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"get-size2",
"schemars",
@@ -3653,7 +3653,7 @@ dependencies = [
[[package]]
name = "ruff_workspace"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"colored 3.1.1",
@@ -4633,7 +4633,7 @@ dependencies = [
[[package]]
name = "ty_module_resolver"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"anyhow",
"camino",
@@ -4828,7 +4828,7 @@ dependencies = [
[[package]]
name = "ty_site_packages"
version = "0.0.0"
version = "0.0.1"
dependencies = [
"camino",
"colored 3.1.1",
+31 -31
View File
@@ -13,48 +13,48 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
ruff = { path = "crates/ruff" }
ruff_annotate_snippets = { path = "crates/ruff_annotate_snippets" }
ruff_cache = { path = "crates/ruff_cache" }
ruff_db = { path = "crates/ruff_db", default-features = false }
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
ruff_formatter = { path = "crates/ruff_formatter" }
ruff_graph = { path = "crates/ruff_graph" }
ruff_index = { path = "crates/ruff_index" }
ruff_linter = { path = "crates/ruff_linter" }
ruff_macros = { path = "crates/ruff_macros" }
ruff_markdown = { path = "crates/ruff_markdown" }
ruff_memory_usage = { path = "crates/ruff_memory_usage" }
ruff_notebook = { path = "crates/ruff_notebook" }
ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
ruff_python_importer = { path = "crates/ruff_python_importer" }
ruff_python_index = { path = "crates/ruff_python_index" }
ruff_python_literal = { path = "crates/ruff_python_literal" }
ruff_python_parser = { path = "crates/ruff_python_parser" }
ruff_python_semantic = { path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { path = "crates/ruff_python_trivia" }
ruff_server = { path = "crates/ruff_server" }
ruff_source_file = { path = "crates/ruff_source_file" }
ruff = { version = "0.15.18", path = "crates/ruff" }
ruff_annotate_snippets = { version = "0.0.1", path = "crates/ruff_annotate_snippets" }
ruff_cache = { version = "0.0.1", path = "crates/ruff_cache" }
ruff_db = { version = "0.0.1", path = "crates/ruff_db", default-features = false }
ruff_diagnostics = { version = "0.0.1", path = "crates/ruff_diagnostics" }
ruff_formatter = { version = "0.0.1", path = "crates/ruff_formatter" }
ruff_graph = { version = "0.0.1", path = "crates/ruff_graph" }
ruff_index = { version = "0.0.1", path = "crates/ruff_index" }
ruff_linter = { version = "0.15.18", path = "crates/ruff_linter" }
ruff_macros = { version = "0.0.1", path = "crates/ruff_macros" }
ruff_markdown = { version = "0.0.1", path = "crates/ruff_markdown" }
ruff_memory_usage = { version = "0.0.1", path = "crates/ruff_memory_usage" }
ruff_notebook = { version = "0.0.1", path = "crates/ruff_notebook" }
ruff_options_metadata = { version = "0.0.1", path = "crates/ruff_options_metadata" }
ruff_python_ast = { version = "0.0.1", path = "crates/ruff_python_ast" }
ruff_python_codegen = { version = "0.0.1", path = "crates/ruff_python_codegen" }
ruff_python_formatter = { version = "0.0.1", path = "crates/ruff_python_formatter" }
ruff_python_importer = { version = "0.0.1", path = "crates/ruff_python_importer" }
ruff_python_index = { version = "0.0.1", path = "crates/ruff_python_index" }
ruff_python_literal = { version = "0.0.1", path = "crates/ruff_python_literal" }
ruff_python_parser = { version = "0.0.1", path = "crates/ruff_python_parser" }
ruff_python_semantic = { version = "0.0.1", path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { version = "0.0.1", path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { version = "0.0.1", path = "crates/ruff_python_trivia" }
ruff_server = { version = "0.0.1", path = "crates/ruff_server" }
ruff_source_file = { version = "0.0.1", path = "crates/ruff_source_file" }
ruff_mdtest = { path = "crates/ruff_mdtest" }
ruff_text_size = { path = "crates/ruff_text_size" }
ruff_workspace = { path = "crates/ruff_workspace" }
ruff_text_size = { version = "0.0.1", path = "crates/ruff_text_size" }
ruff_workspace = { version = "0.0.1", path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
ty_combine = { path = "crates/ty_combine" }
ty_completion_bench = { path = "crates/ty_completion_bench" }
ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_module_resolver = { path = "crates/ty_module_resolver" }
ty_module_resolver = { version = "0.0.1", path = "crates/ty_module_resolver" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
ty_python_core = { path = "crates/ty_python_core" }
ty_server = { path = "crates/ty_server" }
ty_site_packages = { path = "crates/ty_site_packages" }
ty_static = { path = "crates/ty_static" }
ty_site_packages = { version = "0.0.1", path = "crates/ty_site_packages" }
ty_static = { version = "0.0.1", path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { path = "crates/ty_vendored" }
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff"
version = "0.15.18"
publish = true
description = "An extremely fast Python linter and code formatter"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
@@ -9,7 +9,7 @@ homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
readme = "../../README.md"
readme = "README.md"
default-run = "ruff"
[package.metadata.cargo-shear]
+52
View File
@@ -0,0 +1,52 @@
<!-- This file is generated. DO NOT EDIT -->
# Ruff
Ruff is an extremely fast Python linter and code formatter.
See the [documentation](https://docs.astral.sh/ruff/) or
[repository](https://github.com/astral-sh/ruff) for more information.
This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not
considered public interface.
This is version 0.15.18. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff).
The following Ruff workspace members are also available:
- [ruff_annotate_snippets](https://crates.io/crates/ruff_annotate_snippets)
- [ruff_cache](https://crates.io/crates/ruff_cache)
- [ruff_db](https://crates.io/crates/ruff_db)
- [ruff_diagnostics](https://crates.io/crates/ruff_diagnostics)
- [ruff_formatter](https://crates.io/crates/ruff_formatter)
- [ruff_graph](https://crates.io/crates/ruff_graph)
- [ruff_index](https://crates.io/crates/ruff_index)
- [ruff_linter](https://crates.io/crates/ruff_linter)
- [ruff_macros](https://crates.io/crates/ruff_macros)
- [ruff_markdown](https://crates.io/crates/ruff_markdown)
- [ruff_memory_usage](https://crates.io/crates/ruff_memory_usage)
- [ruff_notebook](https://crates.io/crates/ruff_notebook)
- [ruff_options_metadata](https://crates.io/crates/ruff_options_metadata)
- [ruff_python_ast](https://crates.io/crates/ruff_python_ast)
- [ruff_python_codegen](https://crates.io/crates/ruff_python_codegen)
- [ruff_python_formatter](https://crates.io/crates/ruff_python_formatter)
- [ruff_python_importer](https://crates.io/crates/ruff_python_importer)
- [ruff_python_index](https://crates.io/crates/ruff_python_index)
- [ruff_python_literal](https://crates.io/crates/ruff_python_literal)
- [ruff_python_parser](https://crates.io/crates/ruff_python_parser)
- [ruff_python_semantic](https://crates.io/crates/ruff_python_semantic)
- [ruff_python_stdlib](https://crates.io/crates/ruff_python_stdlib)
- [ruff_python_trivia](https://crates.io/crates/ruff_python_trivia)
- [ruff_server](https://crates.io/crates/ruff_server)
- [ruff_source_file](https://crates.io/crates/ruff_source_file)
- [ruff_text_size](https://crates.io/crates/ruff_text_size)
- [ruff_wasm](https://crates.io/crates/ruff_wasm)
- [ruff_workspace](https://crates.io/crates/ruff_workspace)
- [ty_module_resolver](https://crates.io/crates/ty_module_resolver)
- [ty_site_packages](https://crates.io/crates/ty_site_packages)
- [ty_static](https://crates.io/crates/ty_static)
Ruff's workspace members are considered internal and will have frequent breaking changes.
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+3 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_annotate_snippets"
version = "0.1.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
@@ -16,7 +16,7 @@ memchr = { workspace = true }
unicode-width = { workspace = true }
[dev-dependencies]
ruff_annotate_snippets = { workspace = true, features = ["testing-colors"] }
ruff_annotate_snippets = { path = ".", features = ["testing-colors"] }
anstream = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_cache"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_cache
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_cache).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_db"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_db
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_db).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_diagnostics"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_diagnostics
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_diagnostics).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_formatter"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_formatter
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_formatter).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "ruff_graph"
version = "0.1.0"
version = "0.0.1"
description = "This is an internal component crate of Ruff"
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_graph
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_graph).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_index"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_index
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_index).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_linter"
version = "0.15.18"
publish = false
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_linter
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.15.18) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_linter).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_macros"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_macros
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_macros).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "ruff_markdown"
version = "0.0.0"
version = "0.0.1"
description = "This is an internal component crate of Ruff"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_markdown
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_markdown).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_memory_usage"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_memory_usage
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_memory_usage).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_notebook"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_notebook
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_notebook).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_options_metadata"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_options_metadata
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_options_metadata).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_ast"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_ast
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_ast).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
@@ -1,6 +1,7 @@
[package]
name = "ruff_python_ast_integration_tests"
version = "0.0.0"
publish = false
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_codegen"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_codegen
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_codegen).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_formatter"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_importer"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_importer
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_importer).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_index"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_index
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_index).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -3
View File
@@ -1,8 +1,7 @@
[package]
name = "ruff_python_literal"
version = "0.0.0"
publish = false
description = "Common literal handling utilities mostly useful for unparse and repr."
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>", "RustPython Team"]
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_literal
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_literal).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_parser"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>", "RustPython Team"]
edition = { workspace = true }
rust-version = { workspace = true }
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_semantic"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_semantic
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_semantic).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_stdlib"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_stdlib
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_stdlib).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_python_trivia"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_python_trivia
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_python_trivia).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
@@ -1,6 +1,7 @@
[package]
name = "ruff_python_trivia_integration_tests"
version = "0.0.0"
publish = false
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_server"
version = "0.2.2"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_source_file"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_source_file
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_source_file).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_text_size"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_text_size
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_text_size).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_wasm"
version = "0.15.18"
publish = false
description = "WebAssembly bindings for Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
@@ -9,7 +9,6 @@ homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
description = "WebAssembly bindings for Ruff"
[lib]
crate-type = ["cdylib", "rlib"]
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ruff_workspace"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ruff_workspace
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ruff_workspace).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "ty"
version = "0.0.0"
publish = false
# required for correct pypi metadata
homepage = "https://github.com/astral-sh/ty/"
documentation = "https://docs.astral.sh/ty/"
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "ty_combine"
version = "0.0.0"
publish = false
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
+4 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "ty_module_resolver"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
@@ -31,7 +31,8 @@ tracing = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
ty_vendored = { workspace = true }
# Keep this path-only so Cargo strips the unpublished ty dev-dependency when packaging.
ty_vendored = { path = "../ty_vendored" }
anyhow = { workspace = true }
insta = { workspace = true, features = ["filters"] }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ty_module_resolver
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ty_module_resolver).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "ty_project"
version = "0.0.0"
publish = false
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "ty_site_packages"
version = "0.0.0"
publish = false
version = "0.0.1"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ty_site_packages
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ty_site_packages).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "ty_static"
version = "0.0.1"
description = "This is an internal component crate of Ruff"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
+12
View File
@@ -0,0 +1,12 @@
<!-- This file is generated. DO NOT EDIT -->
# ty_static
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.1) is a component of [Ruff 0.15.18](https://crates.io/crates/ruff/0.15.18). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.18/crates/ty_static).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+2 -2
View File
@@ -60,11 +60,11 @@ build-local-artifacts = false
# Local artifacts jobs to run in CI
local-artifacts-jobs = ["./build-binaries", "./build-docker", "./build-wasm"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
publish-jobs = ["./publish-pypi", "./publish-wasm", "./publish-crates"]
# Post-announce jobs to run in CI
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground", "./publish-versions", "./publish-mirror"]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" }, "publish-mirror" = { contents = "read" } }
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" }, "publish-crates" = { contents = "read" }, "publish-mirror" = { contents = "read" } }
# Whether to install an updater program
install-updater = false
# Path that installers should place binaries in
+15
View File
@@ -2,6 +2,21 @@
Ruff uses a custom versioning scheme that uses the **minor** version number for breaking changes and the **patch** version number for bug fixes. Ruff does not yet have a stable API; once Ruff's API is stable, the **major** version number and semantic versioning will be used.
## Crate versioning
Ruff's crates are published to [crates.io](https://crates.io). The following crates follow Ruff's
normal versioning policy:
- `ruff`
- `ruff_linter`
- `ruff_wasm`
The Rust interfaces of these crates do not follow semantic versioning.
The remaining crates published as part of Ruff releases provide **no stability guarantees**. Their
Rust interfaces are considered internal and unstable. Consequently, they are versioned as `0.0.x`.
The patch version is incremented on every Ruff release, regardless of changes to the crate.
## Version changes
**Minor** version increases will occur when:
+120
View File
@@ -0,0 +1,120 @@
# Naively increment the patch version of each crate in the workspace.
#
# This excludes crates which are versioned with Ruff's command-line interface.
#
# After incrementing the version in each member `Cargo.toml`, it updates the version pins in the
# root `Cargo.toml` to match.
# /// script
# requires-python = ">=3.13"
# dependencies = []
# ///
from __future__ import annotations
import json
import pathlib
import subprocess
import tomllib
NO_BUMP_CRATES = {"ruff", "ruff_linter", "ruff_wasm"}
def main() -> None:
# Pre-sync NO_BUMP_CRATES versions before running cargo metadata.
#
# Rooster updates crate versions but isn't workspace-aware, so it doesn't update
# the workspace dependency pins in the root Cargo.toml. This can cause cargo metadata
# to fail with version mismatches (e.g., when going from 0.15.x to 0.16.0).
# We fix the pins first by reading the crate versions directly.
script_dir = pathlib.Path(__file__).parent
workspace_manifest = script_dir.parent / "Cargo.toml"
workspace_manifest_contents = workspace_manifest.read_text()
parsed_workspace_manifest = tomllib.loads(workspace_manifest_contents)
for crate_name in NO_BUMP_CRATES:
crate_manifest = script_dir.parent / "crates" / crate_name / "Cargo.toml"
if not crate_manifest.exists():
continue
crate_version = tomllib.loads(crate_manifest.read_text())["package"]["version"]
manifest_dependency = parsed_workspace_manifest["workspace"][
"dependencies"
].get(crate_name)
if manifest_dependency is None:
continue
manifest_version = manifest_dependency["version"]
if manifest_version != crate_version:
workspace_manifest_contents = workspace_manifest_contents.replace(
f'{crate_name} = {{ version = "{manifest_version}"',
f'{crate_name} = {{ version = "{crate_version}"',
)
workspace_manifest.write_text(workspace_manifest_contents)
# Now cargo metadata will succeed.
result = subprocess.run(
["cargo", "metadata", "--format-version", "1"],
capture_output=True,
text=True,
check=True,
)
content = json.loads(result.stdout)
packages = {package["id"]: package for package in content["packages"]}
workspace_manifest_contents = workspace_manifest.read_text()
parsed_workspace_manifest = tomllib.loads(workspace_manifest_contents)
version_changes = {}
for workspace_member in content["workspace_members"]:
manifest = pathlib.Path(packages[workspace_member]["manifest_path"])
name = packages[workspace_member]["name"]
# Only crates released to crates.io participate in the workspace release sequence.
if packages[workspace_member].get("publish") == []:
continue
# For the members we're not bumping, we'll still make sure that the version pinned in the
# workspace manifest matches the version of the crate. This is done because Rooster isn't
# Cargo workspace aware and won't otherwise bump these when updating the member `Cargo.toml`
# files.
if name in NO_BUMP_CRATES:
manifest_dependency = parsed_workspace_manifest["workspace"][
"dependencies"
].get(name)
if manifest_dependency is None:
continue
manifest_version = manifest_dependency["version"]
metadata_version = packages[workspace_member]["version"]
if manifest_version != metadata_version:
version_changes[name] = (manifest_version, metadata_version)
continue
# For other members, bump the patch version.
version = packages[workspace_member]["version"]
version_parts = [int(part) for part in version.split(".")]
new_version = f"{version_parts[0]}.{version_parts[1]}.{version_parts[2] + 1}"
contents = manifest.read_text()
contents = contents.replace(
'version = "' + version + '"',
'version = "' + new_version + '"',
1,
)
version_changes[name] = (version, new_version)
manifest.write_text(contents)
# Update all the pins in the workspace root.
for name, (old_version, new_version) in version_changes.items():
workspace_manifest_contents = workspace_manifest_contents.replace(
f'{name} = {{ version = "{old_version}"',
f'{name} = {{ version = "{new_version}"',
)
workspace_manifest.write_text(workspace_manifest_contents)
if __name__ == "__main__":
main()
+163
View File
@@ -0,0 +1,163 @@
# /// script
# requires-python = ">=3.13"
# dependencies = []
# ///
from __future__ import annotations
import json
import pathlib
import subprocess
GENERATED_HEADER = "<!-- This file is generated. DO NOT EDIT -->"
RUFF_TEMPLATE = """{GENERATED_HEADER}
# Ruff
Ruff is an extremely fast Python linter and code formatter.
See the [documentation](https://docs.astral.sh/ruff/) or
[repository](https://github.com/astral-sh/ruff) for more information.
This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not
considered public interface.
This is version {ruff_version}. The source can be found [here]({source_url}).
The following Ruff workspace members are also available:
{WORKSPACE_MEMBERS}
Ruff's workspace members are considered internal and will have frequent breaking changes.
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
"""
MEMBER_TEMPLATE = """{GENERATED_HEADER}
# {name}
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version ({crate_version}) is a component of [Ruff {ruff_version}]({ruff_crates_io_url}). The
source can be found [here]({source_url}).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
"""
REPO_URL = "https://github.com/astral-sh/ruff"
PRETTIER_VERSION = "3.8.3"
def main() -> None:
result = subprocess.run(
["cargo", "metadata", "--format-version", "1"],
capture_output=True,
text=True,
check=True,
)
content = json.loads(result.stdout)
packages = {package["id"]: package for package in content["packages"]}
# Find the Ruff version from the ruff crate.
ruff_version = None
for package in content["packages"]:
if package["name"] == "ruff":
ruff_version = package["version"]
break
if ruff_version is None:
raise RuntimeError("Could not find ruff crate")
workspace_root = pathlib.Path(content["workspace_root"])
readme_path = workspace_root / "crates" / "ruff" / "README.md"
workspace_members = []
for workspace_member in content["workspace_members"]:
package = packages[workspace_member]
name = package["name"]
# Skip the main Ruff crate.
if name == "ruff":
continue
# Skip crates with publish = false.
if package.get("publish") == []:
continue
workspace_members.append(name)
workspace_members.sort()
members_list = "\n".join(
f"- [{name}](https://crates.io/crates/{name})" for name in workspace_members
)
# Generate the README for the main Ruff crate.
ruff_source_url = f"{REPO_URL}/blob/{ruff_version}/crates/ruff"
readme_content = RUFF_TEMPLATE.format(
GENERATED_HEADER=GENERATED_HEADER,
WORKSPACE_MEMBERS=members_list,
ruff_version=ruff_version,
source_url=ruff_source_url,
)
readme_path.write_text(readme_content)
# Track all generated README paths for formatting at the end.
generated_paths = [readme_path]
# Generate READMEs for all workspace members.
for workspace_member in content["workspace_members"]:
package = packages[workspace_member]
name = package["name"]
# Skip the main Ruff crate (already handled above).
if name == "ruff":
continue
# Skip crates that aren't released to crates.io.
if package.get("publish") == []:
continue
manifest_path = pathlib.Path(package["manifest_path"])
crate_dir = manifest_path.parent
member_readme_path = crate_dir / "README.md"
# Preserve hand-written crate READMEs.
if member_readme_path.exists():
existing_content = member_readme_path.read_text()
if not existing_content.startswith(GENERATED_HEADER):
print(f"Skipping {name}: existing README without generated header")
continue
crate_version = package["version"]
relative_crate_path = crate_dir.relative_to(workspace_root)
source_url = f"{REPO_URL}/blob/{ruff_version}/{relative_crate_path}"
ruff_crates_io_url = f"https://crates.io/crates/ruff/{ruff_version}"
member_readme_content = MEMBER_TEMPLATE.format(
GENERATED_HEADER=GENERATED_HEADER,
name=name,
crate_version=crate_version,
ruff_version=ruff_version,
ruff_crates_io_url=ruff_crates_io_url,
source_url=source_url,
)
member_readme_path.write_text(member_readme_content)
generated_paths.append(member_readme_path)
print(f"Generated README for {name}")
# Format all generated READMEs once at the end.
subprocess.run(
["npx", "--yes", f"prettier@{PRETTIER_VERSION}", "--write"]
+ [str(path) for path in generated_paths],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if __name__ == "__main__":
main()
+190
View File
@@ -0,0 +1,190 @@
# Publish workspace crates to crates.io idempotently.
#
# `cargo publish --workspace` fails if any selected crate version already exists on crates.io. That
# makes release re-runs fail after a previous partial publish. This script queries crates.io first,
# excludes package versions that already exist, then delegates to `cargo publish --workspace` for the
# remaining packages so Cargo still handles package ordering.
#
# Usage:
#
# CARGO_REGISTRY_TOKEN=<tok> python3 scripts/publish-crates.py --no-verify
from __future__ import annotations
import argparse
import dataclasses
import json
import pathlib
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
CRATES_IO_API = "https://crates.io/api/v1"
USER_AGENT = "ruff-crates-io-publish (github.com/astral-sh/ruff)"
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
# Short delay between crates.io API requests to avoid burst rate limits.
CRATES_IO_REQUEST_DELAY_SECS = 0.2
@dataclasses.dataclass(frozen=True)
class Crate:
name: str
version: str
def pretty(self) -> str:
return f"{self.name}@{self.version}"
def get_publishable_crates(cargo: list[str]) -> list[Crate]:
"""Return publishable workspace crates."""
result = subprocess.run(
[*cargo, "metadata", "--format-version", "1", "--no-deps"],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=True,
)
metadata = json.loads(result.stdout)
workspace_member_ids = set(metadata["workspace_members"])
crates = []
for package in metadata["packages"]:
if package["id"] not in workspace_member_ids:
continue
# `publish = false` is represented as an empty list in cargo metadata.
if package.get("publish") == []:
continue
crates.append(Crate(name=package["name"], version=package["version"]))
return crates
def crate_version_url(crate: Crate, api_url: str) -> str:
crate_name = urllib.parse.quote(crate.name, safe="")
crate_version = urllib.parse.quote(crate.version, safe="")
return f"{api_url}/crates/{crate_name}/{crate_version}"
def crate_version_exists(crate: Crate, api_url: str) -> bool:
"""Return True if the crate version already exists on crates.io."""
request = urllib.request.Request(
crate_version_url(crate, api_url),
headers={"User-Agent": USER_AGENT},
)
try:
with urllib.request.urlopen(request) as response:
if response.status == 200:
return True
raise RuntimeError(
f"unexpected status {response.status} for {crate.pretty()}"
)
except urllib.error.HTTPError as exc:
if exc.code == 404:
return False
raise RuntimeError(
f"failed to query {crate.pretty()} from crates.io: HTTP {exc.code}"
) from exc
except urllib.error.URLError as exc:
raise RuntimeError(
f"failed to query {crate.pretty()} from crates.io: {exc}"
) from exc
def existing_crate_versions(crates: list[Crate], api_url: str) -> set[str]:
"""Return the names of workspace crates whose current version exists on crates.io."""
existing = set()
for index, crate in enumerate(crates):
if index > 0:
time.sleep(CRATES_IO_REQUEST_DELAY_SECS)
if crate_version_exists(crate, api_url):
print(f"Skipping {crate.pretty()}: already exists on crates.io")
existing.add(crate.name)
return existing
def build_cargo_publish_command(
cargo: list[str], existing: set[str], cargo_publish_args: list[str]
) -> list[str]:
"""Build the `cargo publish` command for all not-yet-published workspace crates."""
command = [*cargo, "publish", "--workspace"]
for crate_name in sorted(existing):
command.extend(["--exclude", crate_name])
command.extend(cargo_publish_args)
return command
def publish_workspace(
cargo: list[str], crates: list[Crate], api_url: str, cargo_publish_args: list[str]
) -> int:
existing = existing_crate_versions(crates, api_url)
missing = [crate for crate in crates if crate.name not in existing]
if not missing:
print("All publishable workspace crate versions already exist on crates.io")
return 0
print("Publishing workspace crate versions that do not exist on crates.io:")
for crate in missing:
print(f" {crate.pretty()}")
command = build_cargo_publish_command(cargo, existing, cargo_publish_args)
return subprocess.run(command, cwd=REPO_ROOT).returncode
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Publish workspace crates to crates.io idempotently."
)
parser.add_argument(
"--api-url",
default=CRATES_IO_API,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--cargo",
default=["cargo"],
nargs="+",
help=argparse.SUPPRESS,
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Pass --dry-run through to cargo publish.",
)
parser.add_argument(
"--no-verify",
action="store_true",
help="Pass --no-verify through to cargo publish.",
)
parser.add_argument(
"cargo_publish_args",
nargs=argparse.REMAINDER,
help=argparse.SUPPRESS,
)
args = parser.parse_args(argv)
if args.cargo_publish_args[:1] == ["--"]:
args.cargo_publish_args = args.cargo_publish_args[1:]
return args
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
cargo_publish_args = []
if args.dry_run:
cargo_publish_args.append("--dry-run")
if args.no_verify:
cargo_publish_args.append("--no-verify")
cargo_publish_args.extend(args.cargo_publish_args)
crates = get_publishable_crates(args.cargo)
return publish_workspace(args.cargo, crates, args.api_url, cargo_publish_args)
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "scripts"
version = "0.0.1"
dependencies = ["stdlibs", "tqdm", "mdformat", "pyyaml", "mypy-primer"]
dependencies = ["stdlibs", "tqdm", "mdformat", "pyyaml", "mypy-primer", "httpx"]
requires-python = ">=3.12"
[tool.black]
+9
View File
@@ -14,5 +14,14 @@ cd "$project_root"
uvx --python 3.12 --isolated -- \
rooster@0.1.1 release "$@"
# Bump internal crate versions
uv run --script "$project_root/scripts/bump-workspace-crate-versions.py"
echo "Updating crate READMEs..."
uv run --script "$project_root/scripts/generate-crate-readmes.py"
echo "Updating lockfile..."
cargo update -p ruff
echo "Checking crates.io publish setup..."
uv run --no-config --script "$project_root/scripts/setup-crates-io-publish.py" --quiet
+459
View File
@@ -0,0 +1,459 @@
# Ensure workspace crates are ready for trusted publishing on crates.io.
#
# This script performs four steps for each candidate crate:
#
# 1. Publish a placeholder crate if the crate doesn't yet exist on crates.io.
# 2. Remove trusted publisher configs that don't match our desired config.
# 3. Ensure exactly one desired trusted publishing config exists.
# 4. Enable `trustpub_only` ("Require trusted publishing for all new versions").
#
# It authenticates with `CARGO_REGISTRY_TOKEN`, which must have the `publish-new` and
# `trusted-publishing` scopes.
#
# Crates tracked in `.known-crates` are assumed to be configured and are skipped unless `--force` is
# used.
#
# Usage:
#
# CARGO_REGISTRY_TOKEN=<tok> uv run --script scripts/setup-crates-io-publish.py [--dry-run] [--force] [--quiet]
# /// script
# requires-python = ">=3.13"
# dependencies = ["httpx"]
# ///
from __future__ import annotations
import argparse
import json
import os
import pathlib
import subprocess
import sys
import tempfile
import time
import tomllib
import httpx
CRATES_IO_API = "https://crates.io/api/v1"
USER_AGENT = "ruff-crates-io-publish-setup (github.com/astral-sh/ruff)"
REPOSITORY_OWNER = "astral-sh"
REPOSITORY_NAME = "ruff"
WORKFLOW_FILENAME = "release.yml"
ENVIRONMENT = "release"
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
WORKSPACE_MANIFEST_PATH = REPO_ROOT / "Cargo.toml"
KNOWN_CRATES_PATH = REPO_ROOT / ".known-crates"
KNOWN_CRATES_HEADER = "# GENERATED-BY scripts/setup-crates-io-publish.py\n"
PLACEHOLDER_VERSION = "0.0.0"
# Delay between `cargo publish` calls to respect crates.io rate limits.
PUBLISH_DELAY_SECS = 15
def get_publishable_crates() -> list[dict[str, str]]:
"""Return publishable workspace crates as ``[{"name": …, "version": …}]``."""
result = subprocess.run(
["cargo", "metadata", "--format-version", "1", "--no-deps"],
capture_output=True,
text=True,
check=True,
)
metadata = json.loads(result.stdout)
workspace_member_ids = set(metadata["workspace_members"])
crates = []
for package in metadata["packages"]:
if package["id"] not in workspace_member_ids:
continue
# ``publish = false`` is represented as an empty list in cargo metadata.
if package.get("publish") == []:
continue
crates.append({"name": package["name"], "version": package["version"]})
return sorted(crates, key=lambda c: c["name"])
def load_known_crates() -> set[str]:
"""Load crate names from ``.known-crates``."""
if not KNOWN_CRATES_PATH.exists():
return set()
known = set()
for line in KNOWN_CRATES_PATH.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
known.add(line)
return known
def save_known_crates(crates: set[str]) -> None:
"""Persist the sorted set of fully-configured crate names."""
lines = [KNOWN_CRATES_HEADER]
for name in sorted(crates):
lines.append(f"{name}\n")
KNOWN_CRATES_PATH.write_text("".join(lines))
def get_crate_metadata(
client: httpx.Client, crate_name: str
) -> dict[str, object] | None:
"""Get crate metadata from the public crates.io API."""
response = client.get(f"{CRATES_IO_API}/crates/{crate_name}")
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
def load_workspace_package_metadata() -> dict[str, object]:
"""Load shared package metadata from the workspace manifest."""
manifest = tomllib.loads(WORKSPACE_MANIFEST_PATH.read_text())
workspace_package = manifest.get("workspace", {}).get("package")
if not isinstance(workspace_package, dict):
raise RuntimeError("workspace.package is missing from Cargo.toml")
return workspace_package
def publish_placeholder_crate(
crate_name: str,
workspace_package: dict[str, object],
) -> bool:
"""Publish a generated placeholder crate to reserve a new crates.io name."""
with tempfile.TemporaryDirectory(prefix=f"{crate_name}-placeholder-") as temp_dir:
temp_path = pathlib.Path(temp_dir)
src_dir = temp_path / "src"
src_dir.mkdir(parents=True)
authors = workspace_package.get("authors", [])
if not isinstance(authors, list):
raise RuntimeError("workspace.package.authors must be a list")
edition = workspace_package.get("edition")
rust_version = workspace_package.get("rust-version")
homepage = workspace_package.get("homepage")
repository = workspace_package.get("repository")
license_expression = workspace_package.get("license")
description = (
"This is a placeholder release for an internal component crate of Ruff"
)
manifest = (
"[package]\n"
f"name = {json.dumps(crate_name)}\n"
f"version = {json.dumps(PLACEHOLDER_VERSION)}\n"
f"edition = {json.dumps(edition)}\n"
f"rust-version = {json.dumps(rust_version)}\n"
f"authors = {json.dumps(authors)}\n"
f"license = {json.dumps(license_expression)}\n"
f"homepage = {json.dumps(homepage)}\n"
f"repository = {json.dumps(repository)}\n"
f"description = {json.dumps(description)}\n"
'readme = "README.md"\n'
)
(temp_path / "Cargo.toml").write_text(manifest)
(temp_path / "README.md").write_text(
"<!-- This file is generated. DO NOT EDIT -->\n\n"
f"# {crate_name}\n\n"
f"This crate is an internal component of [Ruff](https://crates.io/crates/ruff). "
f"This placeholder version ({PLACEHOLDER_VERSION}) only exists to reserve the "
"crate name and enable trusted publishing for future releases.\n"
)
(src_dir / "lib.rs").write_text(
"//! Placeholder crate published to reserve the name on crates.io.\n\n"
"/// Marker type for the placeholder release.\n"
"pub struct Placeholder;\n"
)
result = subprocess.run(
[
"cargo",
"publish",
"--manifest-path",
str(temp_path / "Cargo.toml"),
"--no-verify",
],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(result.stderr, file=sys.stderr, end="")
return False
return True
def create_trusted_publisher(client: httpx.Client, crate_name: str) -> None:
"""Create a Trusted Publishing GitHub config for *crate_name*."""
response = client.post(
f"{CRATES_IO_API}/trusted_publishing/github_configs",
json={
"github_config": {
"crate": crate_name,
"repository_owner": REPOSITORY_OWNER,
"repository_name": REPOSITORY_NAME,
"workflow_filename": WORKFLOW_FILENAME,
"environment": ENVIRONMENT,
}
},
)
response.raise_for_status()
def list_trusted_publishers(
client: httpx.Client, crate_name: str
) -> list[dict[str, object]]:
"""List Trusted Publishing GitHub configs for *crate_name*."""
response = client.get(
f"{CRATES_IO_API}/trusted_publishing/github_configs",
params={"crate": crate_name},
)
response.raise_for_status()
payload = response.json()
return payload.get("github_configs", [])
def delete_trusted_publisher(client: httpx.Client, config_id: int) -> None:
"""Delete a Trusted Publishing GitHub config by ID."""
response = client.delete(
f"{CRATES_IO_API}/trusted_publishing/github_configs/{config_id}"
)
response.raise_for_status()
def set_trustpub_only(client: httpx.Client, crate_name: str, enabled: bool) -> None:
"""Enable or disable `trustpub_only` for a crate."""
response = client.patch(
f"{CRATES_IO_API}/crates/{crate_name}",
json={"crate": {"trustpub_only": enabled}},
)
response.raise_for_status()
def is_desired_config(config: dict[str, object]) -> bool:
"""Return True if *config* matches the workflow this repo uses."""
return (
config.get("repository_owner") == REPOSITORY_OWNER
and config.get("repository_name") == REPOSITORY_NAME
and config.get("workflow_filename") == WORKFLOW_FILENAME
and config.get("environment") == ENVIRONMENT
)
def handle_trusted_publisher_error(exc: httpx.HTTPStatusError) -> None:
"""Print a helpful hint for common trusted-publishing API errors, then exit."""
print(
f"error {exc.response.status_code}: {exc.response.text}",
file=sys.stderr,
)
if exc.response.status_code == 401:
if "GitHub session has expired" in exc.response.text:
print(
"\nhint: your crates.io account's linked GitHub OAuth"
" token is stale.\nLog out and back in at"
" https://crates.io to refresh it, then re-run.",
file=sys.stderr,
)
else:
print(
"\nhint: your CARGO_REGISTRY_TOKEN may be invalid or revoked.",
file=sys.stderr,
)
elif exc.response.status_code == 403:
print(
"\nhint: your token may lack the trusted-publishing scope,"
" or you are not an owner of this crate.",
file=sys.stderr,
)
sys.exit(1)
def main() -> None:
parser = argparse.ArgumentParser(
description="Ensure workspace crates are ready for trusted publishing on crates.io."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without making changes",
)
parser.add_argument(
"--force", action="store_true", help="Re-check crates already in .known-crates"
)
parser.add_argument(
"--quiet", "-q", action="store_true", help="Suppress informational output"
)
args = parser.parse_args()
dry_run = args.dry_run
force = args.force
quiet = args.quiet
workspace_package = load_workspace_package_metadata()
crates = get_publishable_crates()
known = set() if force else load_known_crates()
candidates = [c for c in crates if c["name"] not in known]
if not candidates:
if not quiet:
print(
f"All {len(crates)} publishable crates are in .known-crates — nothing to do."
)
return
token = os.environ.get("CARGO_REGISTRY_TOKEN", "")
if not token:
crate_names = ", ".join(crate["name"] for crate in candidates)
print(
f"Crates requiring crates.io publish setup: {crate_names}",
file=sys.stderr,
)
print(
"error: CARGO_REGISTRY_TOKEN is required (with `publish-new`"
" and `trusted-publishing` scopes)",
file=sys.stderr,
)
print(
"\nPlease ask a crates.io owner to bootstrap releases which add a new crate.",
file=sys.stderr,
)
sys.exit(1)
client = httpx.Client(headers={"User-Agent": USER_AGENT}, timeout=30)
auth_client = httpx.Client(
headers={"Authorization": token, "User-Agent": USER_AGENT},
timeout=30,
)
s = "" if len(candidates) == 1 else "s"
if not quiet:
print(f"Checking {len(candidates)} crate{s}")
published_any = False
for crate in candidates:
name = crate["name"]
version = crate["version"]
metadata = get_crate_metadata(client, name)
exists = metadata is not None
trustpub_only_enabled = False
if exists:
crate_payload = metadata.get("crate")
if isinstance(crate_payload, dict):
trustpub_only_enabled = bool(crate_payload.get("trustpub_only", False))
configs: list[dict[str, object]] = []
if exists:
try:
configs = list_trusted_publishers(auth_client, name)
except httpx.HTTPStatusError as exc:
print(f"{name}: failed to list trusted publishers", file=sys.stderr)
handle_trusted_publisher_error(exc)
desired_configs = [config for config in configs if is_desired_config(config)]
extra_desired_configs = desired_configs[1:]
non_matching_configs = [
config for config in configs if not is_desired_config(config)
]
configs_to_delete = non_matching_configs + extra_desired_configs
needs_initial_publish = not exists
needs_add_publisher = not desired_configs
needs_trustpub_only = not trustpub_only_enabled
if dry_run:
actions: list[str] = []
if needs_initial_publish:
actions.append("publish placeholder")
if configs_to_delete:
count = len(configs_to_delete)
noun = "publisher" if count == 1 else "publishers"
actions.append(f"remove {count} {noun}")
if needs_add_publisher:
actions.append("add trusted publisher")
if needs_trustpub_only:
actions.append("enable trustpub_only")
if actions:
print(f"{name}: would {' and '.join(actions)}")
elif not quiet:
print(f"{name}: already configured")
continue
if needs_initial_publish:
if version == PLACEHOLDER_VERSION:
print(
f"{name}: workspace version matches placeholder version {PLACEHOLDER_VERSION}; "
"bump the real crate version before running this script",
file=sys.stderr,
)
sys.exit(1)
if published_any:
print(f"waiting {PUBLISH_DELAY_SECS}s for rate limit")
time.sleep(PUBLISH_DELAY_SECS)
print(f"{name}: publishing placeholder")
if not publish_placeholder_crate(name, workspace_package):
print(f"{name}: placeholder publish failed", file=sys.stderr)
sys.exit(1)
published_any = True
if configs_to_delete:
deleted = 0
for config in configs_to_delete:
config_id = config.get("id")
if not isinstance(config_id, int):
print(
f"{name}: unexpected trusted publisher payload: missing `id`",
file=sys.stderr,
)
sys.exit(1)
try:
delete_trusted_publisher(auth_client, config_id)
except httpx.HTTPStatusError as exc:
print(
f"{name}: failed to delete trusted publisher {config_id}",
file=sys.stderr,
)
handle_trusted_publisher_error(exc)
deleted += 1
noun = "publisher" if deleted == 1 else "publishers"
print(f"{name}: removed {deleted} {noun}")
if needs_add_publisher:
print(f"{name}: registering trusted publisher")
try:
create_trusted_publisher(auth_client, name)
except httpx.HTTPStatusError as exc:
handle_trusted_publisher_error(exc)
if needs_trustpub_only:
print(f"{name}: enabling trustpub_only")
try:
set_trustpub_only(auth_client, name, enabled=True)
except httpx.HTTPStatusError as exc:
handle_trusted_publisher_error(exc)
if (
not configs_to_delete
and not needs_add_publisher
and not needs_trustpub_only
and not quiet
):
print(f"{name}: already configured")
known.add(name)
if not dry_run:
save_known_crates(known)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
version = 1
revision = 3
requires-python = ">=3.13"
[manifest]
requirements = [{ name = "httpx" }]
[[package]]
name = "anyio"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
]
[[package]]
name = "certifi"
version = "2026.5.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
+79
View File
@@ -2,6 +2,28 @@ version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "anyio"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
]
[[package]]
name = "certifi"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -11,6 +33,52 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
@@ -100,6 +168,7 @@ name = "scripts"
version = "0.0.1"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
{ name = "mdformat" },
{ name = "mypy-primer" },
{ name = "pyyaml" },
@@ -109,6 +178,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "httpx" },
{ name = "mdformat" },
{ name = "mypy-primer", git = "https://github.com/hauntsaninja/mypy_primer" },
{ name = "pyyaml" },
@@ -136,3 +206,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]