mirror of https://github.com/astral-sh/uv
Publish: Support --index <name> (#9694)
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 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
a090cf1f12
commit
321101d340
|
|
@ -5180,14 +5180,32 @@ pub struct PublishArgs {
|
|||
#[arg(default_value = "dist/*")]
|
||||
pub files: Vec<String>,
|
||||
|
||||
/// The URL of the upload endpoint (not the index URL).
|
||||
/// The name of an index in the configuration to use for publishing.
|
||||
///
|
||||
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
|
||||
/// and index upload.
|
||||
/// The index must have a `publish-url` setting, for example:
|
||||
///
|
||||
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
|
||||
#[arg(long, env = EnvVars::UV_PUBLISH_URL)]
|
||||
pub publish_url: Option<Url>,
|
||||
/// ```toml
|
||||
/// [[tool.uv.index]]
|
||||
/// name = "pypi"
|
||||
/// url = "https://pypi.org/simple"
|
||||
/// publish-url = "https://upload.pypi.org/legacy/"
|
||||
/// ```
|
||||
///
|
||||
/// The index `url` will be used to check for existing files to skip duplicate uploads.
|
||||
///
|
||||
/// With these settings, the following two calls are equivalent:
|
||||
///
|
||||
/// ```
|
||||
/// uv publish --index pypi
|
||||
/// uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pypi.org/simple
|
||||
/// ```
|
||||
#[arg(
|
||||
long,
|
||||
env = EnvVars::UV_PUBLISH_INDEX,
|
||||
conflicts_with = "publish_url",
|
||||
conflicts_with = "check_url"
|
||||
)]
|
||||
pub index: Option<String>,
|
||||
|
||||
/// The username for the upload.
|
||||
#[arg(short, long, env = EnvVars::UV_PUBLISH_USERNAME)]
|
||||
|
|
@ -5227,6 +5245,15 @@ pub struct PublishArgs {
|
|||
#[arg(long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER)]
|
||||
pub keyring_provider: Option<KeyringProviderType>,
|
||||
|
||||
/// The URL of the upload endpoint (not the index URL).
|
||||
///
|
||||
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
|
||||
/// and index upload.
|
||||
///
|
||||
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
|
||||
#[arg(long, env = EnvVars::UV_PUBLISH_URL)]
|
||||
pub publish_url: Option<Url>,
|
||||
|
||||
/// Check an index URL for existing files to skip duplicate uploads.
|
||||
///
|
||||
/// This option allows retrying publishing that failed after only some, but not all files have
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
@ -67,6 +68,19 @@ pub struct Index {
|
|||
// /// can point to either local or remote resources.
|
||||
// #[serde(default)]
|
||||
// pub r#type: IndexKind,
|
||||
/// The URL of the upload endpoint.
|
||||
///
|
||||
/// When using `uv publish --index <name>`, this URL is used for publishing.
|
||||
///
|
||||
/// A configuration for the default index PyPI would look as follows:
|
||||
///
|
||||
/// ```toml
|
||||
/// [[tool.uv.index]]
|
||||
/// name = "pypi"
|
||||
/// url = "https://pypi.org/simple"
|
||||
/// publish-url = "https://upload.pypi.org/legacy/"
|
||||
/// ```
|
||||
pub publish_url: Option<Url>,
|
||||
}
|
||||
|
||||
// #[derive(
|
||||
|
|
@ -90,6 +104,7 @@ impl Index {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +116,7 @@ impl Index {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +128,7 @@ impl Index {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +183,7 @@ impl FromStr for Index {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -178,6 +196,7 @@ impl FromStr for Index {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ impl EnvVars {
|
|||
/// will use this token (with the username `__token__`) for publishing.
|
||||
pub const UV_PUBLISH_TOKEN: &'static str = "UV_PUBLISH_TOKEN";
|
||||
|
||||
/// Equivalent to the `--index` command-line argument in `uv publish`. If
|
||||
/// set, uv the index with this name in the configuration for publishing.
|
||||
pub const UV_PUBLISH_INDEX: &'static str = "UV_PUBLISH_INDEX";
|
||||
|
||||
/// Equivalent to the `--username` command-line argument in `uv publish`. If
|
||||
/// set, uv will use this username for publishing.
|
||||
pub const UV_PUBLISH_USERNAME: &'static str = "UV_PUBLISH_USERNAME";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use std::process::ExitCode;
|
|||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anstream::eprintln;
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::error::{ContextKind, ContextValue};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use owo_colors::OwoColorize;
|
||||
|
|
@ -1210,8 +1210,46 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
trusted_publishing,
|
||||
keyring_provider,
|
||||
check_url,
|
||||
index,
|
||||
index_locations,
|
||||
} = PublishSettings::resolve(args, filesystem);
|
||||
|
||||
let (publish_url, check_url) = if let Some(index_name) = index {
|
||||
debug!("Publishing with index {index_name}");
|
||||
let index = index_locations
|
||||
.indexes()
|
||||
.find(|index| {
|
||||
index
|
||||
.name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name.as_ref() == 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 is missing a publish URL: `{index_name}`"))?;
|
||||
let check_url = index.url.clone();
|
||||
(publish_url, Some(check_url))
|
||||
} else {
|
||||
(publish_url, check_url)
|
||||
};
|
||||
|
||||
commands::publish(
|
||||
files,
|
||||
publish_url,
|
||||
|
|
|
|||
|
|
@ -2854,12 +2854,16 @@ pub(crate) struct PublishSettings {
|
|||
pub(crate) files: Vec<String>,
|
||||
pub(crate) username: Option<String>,
|
||||
pub(crate) password: Option<String>,
|
||||
pub(crate) index: Option<String>,
|
||||
|
||||
// Both CLI and configuration.
|
||||
pub(crate) publish_url: Url,
|
||||
pub(crate) trusted_publishing: TrustedPublishing,
|
||||
pub(crate) keyring_provider: KeyringProviderType,
|
||||
pub(crate) check_url: Option<IndexUrl>,
|
||||
|
||||
// Configuration only
|
||||
pub(crate) index_locations: IndexLocations,
|
||||
}
|
||||
|
||||
impl PublishSettings {
|
||||
|
|
@ -2877,7 +2881,11 @@ impl PublishSettings {
|
|||
check_url,
|
||||
} = publish;
|
||||
let ResolverInstallerOptions {
|
||||
keyring_provider, ..
|
||||
keyring_provider,
|
||||
index,
|
||||
extra_index_url,
|
||||
index_url,
|
||||
..
|
||||
} = top_level;
|
||||
|
||||
// Tokens are encoded in the same way as username/password
|
||||
|
|
@ -2903,6 +2911,17 @@ impl PublishSettings {
|
|||
.combine(keyring_provider)
|
||||
.unwrap_or_default(),
|
||||
check_url: args.check_url.combine(check_url),
|
||||
index: args.index,
|
||||
index_locations: IndexLocations::new(
|
||||
index
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(extra_index_url.into_iter().flatten().map(Index::from))
|
||||
.chain(index_url.into_iter().map(Index::from))
|
||||
.collect(),
|
||||
Vec::new(),
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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`
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -272,6 +273,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -427,6 +429,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -614,6 +617,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -906,6 +910,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -1085,6 +1090,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -1113,6 +1119,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -1271,6 +1278,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -1299,6 +1307,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -1327,6 +1336,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -1507,6 +1517,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
no_index: true,
|
||||
|
|
@ -1826,6 +1837,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -1854,6 +1866,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -2008,6 +2021,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -2036,6 +2050,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -3089,6 +3104,7 @@ fn resolve_both() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -3366,6 +3382,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4058,6 +4075,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -4086,6 +4104,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4242,6 +4261,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -4270,6 +4290,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4432,6 +4453,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -4460,6 +4482,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4617,6 +4640,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -4645,6 +4669,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4809,6 +4834,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -4837,6 +4863,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
@ -4994,6 +5021,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
origin: Some(
|
||||
Cli,
|
||||
),
|
||||
publish_url: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
|
@ -5022,6 +5050,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
explicit: false,
|
||||
default: true,
|
||||
origin: None,
|
||||
publish_url: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -8152,6 +8152,15 @@ uv publish [OPTIONS] [FILES]...
|
|||
|
||||
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
||||
|
||||
</dd><dt><code>--index</code> <i>index</i></dt><dd><p>The name of an index in the configuration to use for publishing.</p>
|
||||
|
||||
<p>The index must have a <code>publish-url</code> setting, for example:</p>
|
||||
|
||||
<pre><code class="language-toml [[tool.uv.index]] name = "pypi" url = "https://pypi.org/simple" publish-url = "https://upload.pypi.org/legacy/" ```">The index `url` will be used to check for existing files to skip duplicate uploads.
|
||||
|
||||
With these settings, the following two calls are equivalent:</code></pre>
|
||||
|
||||
<p>May also be set with the <code>UV_PUBLISH_INDEX</code> environment variable.</p>
|
||||
</dd><dt><code>--keyring-provider</code> <i>keyring-provider</i></dt><dd><p>Attempt to use <code>keyring</code> for authentication for remote requirements files.</p>
|
||||
|
||||
<p>At present, only <code>--keyring-provider subprocess</code> is supported, which configures uv to use the <code>keyring</code> CLI to handle authentication.</p>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,17 @@ class TargetConfiguration:
|
|||
project_name: str
|
||||
publish_url: str
|
||||
index_url: str
|
||||
index: str | None = None
|
||||
|
||||
def index_declaration(self) -> str | None:
|
||||
if not self.index:
|
||||
return None
|
||||
return (
|
||||
"[[tool.uv.index]]\n"
|
||||
+ f'name = "{self.index}"\n'
|
||||
+ f'url = "{self.index_url}"\n'
|
||||
+ f'publish-url = "{self.publish_url}"\n'
|
||||
)
|
||||
|
||||
|
||||
# Map CLI target name to package name and index url.
|
||||
|
|
@ -114,6 +125,7 @@ local_targets: dict[str, TargetConfiguration] = {
|
|||
"astral-test-token",
|
||||
TEST_PYPI_PUBLISH_URL,
|
||||
"https://test.pypi.org/simple/",
|
||||
"test-pypi",
|
||||
),
|
||||
"pypi-password-env": TargetConfiguration(
|
||||
"astral-test-password",
|
||||
|
|
@ -209,10 +221,12 @@ def get_filenames(url: str, client: httpx.Client) -> list[str]:
|
|||
|
||||
|
||||
def build_project_at_version(
|
||||
project_name: str, version: Version, uv: Path, modified: bool = False
|
||||
target: str, version: Version, uv: Path, modified: bool = False
|
||||
) -> Path:
|
||||
"""Build a source dist and a wheel with the project name and an unclaimed
|
||||
version."""
|
||||
project_name = all_targets[target].project_name
|
||||
|
||||
if modified:
|
||||
dir_name = f"{project_name}-modified"
|
||||
else:
|
||||
|
|
@ -225,7 +239,7 @@ def build_project_at_version(
|
|||
[uv, "init", "-p", PYTHON_VERSION, "--lib", "--name", project_name, dir_name],
|
||||
cwd=cwd,
|
||||
)
|
||||
project_root.joinpath("pyproject.toml").write_text(
|
||||
toml = (
|
||||
"[project]\n"
|
||||
+ f'name = "{project_name}"\n'
|
||||
# Set to an unclaimed version
|
||||
|
|
@ -233,6 +247,10 @@ def build_project_at_version(
|
|||
# Add all supported metadata
|
||||
+ PYPROJECT_TAIL
|
||||
)
|
||||
if index_declaration := all_targets[target].index_declaration():
|
||||
toml += index_declaration
|
||||
|
||||
project_root.joinpath("pyproject.toml").write_text(toml)
|
||||
shutil.copy(
|
||||
cwd.parent.parent.joinpath("LICENSE-APACHE"),
|
||||
cwd.joinpath(dir_name).joinpath("LICENSE-APACHE"),
|
||||
|
|
@ -320,7 +338,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
|
|||
# The distributions are build to the dist directory of the project.
|
||||
previous_version = get_latest_version(project_name, client)
|
||||
version = get_new_version(previous_version)
|
||||
project_dir = build_project_at_version(project_name, version, uv)
|
||||
project_dir = build_project_at_version(target, version, uv)
|
||||
|
||||
# Upload configuration
|
||||
publish_url = all_targets[target].publish_url
|
||||
|
|
@ -358,17 +376,28 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
|
|||
f"---\n{output}\n---"
|
||||
)
|
||||
|
||||
print(f"\n=== 3. Publishing {project_name} {version} again with check URL ===")
|
||||
mode = "index" if all_targets[target].index else "check URL"
|
||||
print(f"\n=== 3. Publishing {project_name} {version} again with {mode} ===")
|
||||
wait_for_index(index_url, project_name, version, uv)
|
||||
args = [
|
||||
uv,
|
||||
"publish",
|
||||
"--publish-url",
|
||||
publish_url,
|
||||
"--check-url",
|
||||
index_url,
|
||||
*extra_args,
|
||||
]
|
||||
# Test twine-style and index-style uploads for different packages.
|
||||
if index := all_targets[target].index:
|
||||
args = [
|
||||
uv,
|
||||
"publish",
|
||||
"--index",
|
||||
index,
|
||||
*extra_args,
|
||||
]
|
||||
else:
|
||||
args = [
|
||||
uv,
|
||||
"publish",
|
||||
"--publish-url",
|
||||
publish_url,
|
||||
"--check-url",
|
||||
index_url,
|
||||
*extra_args,
|
||||
]
|
||||
output = run(
|
||||
args, cwd=project_dir, env=env, text=True, check=True, stderr=PIPE
|
||||
).stderr
|
||||
|
|
@ -385,9 +414,7 @@ def publish_project(target: str, uv: Path, client: httpx.Client):
|
|||
|
||||
# Build a different source dist and wheel at the same version, so the upload fails
|
||||
del project_dir
|
||||
modified_project_dir = build_project_at_version(
|
||||
project_name, version, uv, modified=True
|
||||
)
|
||||
modified_project_dir = build_project_at_version(target, version, uv, modified=True)
|
||||
|
||||
print(
|
||||
f"\n=== 4. Publishing modified {project_name} {version} "
|
||||
|
|
|
|||
|
|
@ -685,6 +685,14 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"publish-url": {
|
||||
"description": "The URL of the upload endpoint.\n\nWhen using `uv publish --index <name>`, this URL is used for publishing.\n\nA configuration for the default index PyPI would look as follows:\n\n```toml [[tool.uv.index]] name = \"pypi\" url = \"https://pypi.org/simple\" publish-url = \"https://upload.pypi.org/legacy/\" ```",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uri"
|
||||
},
|
||||
"url": {
|
||||
"description": "The URL of the index.\n\nExpects to receive a URL (e.g., `https://pypi.org/simple`) or a local path.",
|
||||
"allOf": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue