Skip to content

Commit

Permalink
Support uv publish --index
Browse files Browse the repository at this point in the history
When publishing, we currently ask the user to set `--publish-url` to the upload URl and `--check-url` to the simple index URL, or the equivalent configuration keys. But that's redundant with the `[[tool.uv.index]]` declaration. Instead, we extend `[[tool.uv.index]]` with a `publish-url` entry and allow passing `uv publish --index <name>`.

`uv publish --index <name>` requires the `pyproject.toml` to be present when publishing, unlike using `--publish-url ... --check-url ...` which can be used e.g. in CI without a checkout step. `--index` also always uses the check URL feature to aid upload consistency.

The documentation tries to explain both approaches together, which overlap for the check URL feature.

Fixes #8864
  • Loading branch information
konstin committed Dec 6, 2024
1 parent 896d540 commit af606c7
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 22 deletions.
1 change: 1 addition & 0 deletions crates/uv-distribution-types/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{IndexUrl, IndexUrlError};

#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Index {
/// The name of the index.
///
Expand Down
19 changes: 17 additions & 2 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,11 +1217,26 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.as_ref()
.is_some_and(|name| name.as_ref() == index_name)
})
.with_context(|| format!("No such index {index_name}"))?;
.with_context(|| {
let mut index_names: Vec<String> = index_locations
.indexes()
.filter_map(|index| index.name.as_ref())
.map(ToString::to_string)
.collect();
index_names.sort();
if index_names.is_empty() {
format!("No indexes were found, can't use index: `{index_name}`")
} else {
let index_names = index_names.join("`, `");
format!(
"Index not found: `{index_name}`. Found indexes: `{index_names}`"
)
}
})?;
let publish_url = index
.publish_url
.clone()
.with_context(|| format!("Index {index_name} is missing a publish URL"))?;
.with_context(|| format!("Index is missing a publish URL: `{index_name}`"))?;
let check_url = index.url.clone();
(publish_url, Some(check_url))
} else {
Expand Down
72 changes: 71 additions & 1 deletion crates/uv/tests/it/publish.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::common::{uv_snapshot, venv_bin_path, TestContext};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileTouch, PathChild};
use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild};
use indoc::indoc;
use std::env;
use std::env::current_dir;
use uv_static::EnvVars;

#[test]
Expand Down Expand Up @@ -324,3 +326,71 @@ fn check_keyring_behaviours() {
"###
);
}

#[test]
fn invalid_index() {
let context = TestContext::new("3.12");

let pyproject_toml = indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
[[tool.uv.index]]
name = "foo"
url = "https://example.com"
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.org"
"#};
context
.temp_dir
.child("pyproject.toml")
.write_str(pyproject_toml)
.unwrap();

let ok_wheel = current_dir()
.unwrap()
.join("../../scripts/links/ok-1.0.0-py3-none-any.whl");

// No such index
uv_snapshot!(context.filters(), context.publish()
.arg("-u")
.arg("__token__")
.arg("-p")
.arg("dummy")
.arg("--index")
.arg("bar")
.arg(&ok_wheel)
.current_dir(context.temp_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv publish` is experimental and may change without warning
error: Index not found: `bar`. Found indexes: `foo`, `internal`
"###
);

// Index does not have a publish URL
uv_snapshot!(context.filters(), context.publish()
.arg("-u")
.arg("__token__")
.arg("-p")
.arg("dummy")
.arg("--index")
.arg("foo")
.arg(&ok_wheel)
.current_dir(context.temp_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv publish` is experimental and may change without warning
error: Index is missing a publish URL: `foo`
"###
);
}
29 changes: 29 additions & 0 deletions crates/uv/tests/it/show_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -272,6 +273,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -427,6 +429,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -614,6 +617,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -906,6 +910,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -1085,6 +1090,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -1113,6 +1119,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -1271,6 +1278,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -1299,6 +1307,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -1327,6 +1336,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -1507,6 +1517,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
],
no_index: true,
Expand Down Expand Up @@ -1826,6 +1837,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -1854,6 +1866,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -2008,6 +2021,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -2036,6 +2050,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -3089,6 +3104,7 @@ fn resolve_both() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -3366,6 +3382,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4058,6 +4075,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -4086,6 +4104,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4242,6 +4261,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -4270,6 +4290,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4432,6 +4453,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -4460,6 +4482,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4617,6 +4640,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -4645,6 +4669,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4809,6 +4834,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -4837,6 +4863,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down Expand Up @@ -4994,6 +5021,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
publish_url: None,
},
Index {
name: None,
Expand Down Expand Up @@ -5022,6 +5050,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
publish_url: None,
},
],
flat_index: [],
Expand Down
5 changes: 5 additions & 0 deletions docs/configuration/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ for more details.

Don't upload a file if it already exists on the index. The value is the URL of the index.

### `UV_PUBLISH_INDEX`

Equivalent to the `--index` command-line argument in `uv publish`. If
set, uv the index with this name in the configuration for publishing.

### `UV_PUBLISH_PASSWORD`

Equivalent to the `--password` command-line argument in `uv publish`. If
Expand Down
25 changes: 20 additions & 5 deletions docs/guides/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,29 @@ PyPI from GitHub Actions, you don't need to set any credentials. Instead,
generate a token. Using a token is equivalent to setting `--username __token__` and using the
token as password.

If you're using a custom index through `[[tool.uv.index]]`, add `publish-url` and use
`uv publish --index <name>`. For example:

```toml
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
```

!!! note

When using `uv publish --index <name>`, the `pyproject.toml` must be present, i.e. you need to
have a checkout step in a publish CI job.

Even though `uv publish` retries failed uploads, it can happen that publishing fails in the middle,
with some files uploaded and some files still missing. With PyPI, you can retry the exact same
command, existing identical files will be ignored. With other registries, use
`--check-url <index url>` with the index URL (not the publish URL) the packages belong to. uv will
skip uploading files that are identical to files in the registry, and it will also handle raced
parallel uploads. Note that existing files need to match exactly with those previously uploaded to
the registry, this avoids accidentally publishing source distribution and wheels with different
contents for the same version.
`--check-url <index url>` with the index URL (not the publish URL) the packages belong to. When
using `--index`, the index URL is used as check URL. uv will skip uploading files that are identical
to files in the registry, and it will also handle raced parallel uploads. Note that existing files
need to match exactly with those previously uploaded to the registry, this avoids accidentally
publishing source distribution and wheels with different contents for the same version.

## Installing your package

Expand Down
Loading

0 comments on commit af606c7

Please sign in to comment.