mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
Add uv publish --dry-run (#15638)
## Summary `uv publish --dry-run` will perform the `--check-url` validation, and hit the `/validate` endpoint if the registry is known to support fast-path validation (like pyx). The `/validate` endpoint lets us validate an upload without uploading the file _contents_, which lets you skip the expensive step for common mistakes. In the future, my hope is that the `/validate` step will deprecated in favor of Upload API 2.0.
This commit is contained in:
parent
b57ad179b6
commit
7606f1ad3c
6 changed files with 187 additions and 25 deletions
|
|
@ -6572,6 +6572,13 @@ pub struct PublishArgs {
|
||||||
|
|
||||||
#[arg(long, hide = true)]
|
#[arg(long, hide = true)]
|
||||||
pub skip_existing: bool,
|
pub skip_existing: bool,
|
||||||
|
|
||||||
|
/// Perform a dry run without uploading files.
|
||||||
|
///
|
||||||
|
/// When enabled, the command will check for existing files if `--check-url` is provided,
|
||||||
|
/// and will perform validation against the index if supported, but will not upload any files.
|
||||||
|
#[arg(long)]
|
||||||
|
pub dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [PEP 517](https://peps.python.org/pep-0517/) and
|
/// See [PEP 517](https://peps.python.org/pep-0517/) and
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ use tracing::{Level, debug, enabled, trace, warn};
|
||||||
use trusted_publishing::TrustedPublishingToken;
|
use trusted_publishing::TrustedPublishingToken;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use uv_auth::Credentials;
|
use uv_auth::{Credentials, PyxTokenStore};
|
||||||
use uv_cache::{Cache, Refresh};
|
use uv_cache::{Cache, Refresh};
|
||||||
use uv_client::{
|
use uv_client::{
|
||||||
BaseClient, MetadataFormat, OwnedArchive, RegistryClientBuilder, RequestBuilder,
|
BaseClient, MetadataFormat, OwnedArchive, RegistryClientBuilder, RequestBuilder,
|
||||||
|
|
@ -60,6 +60,8 @@ pub enum PublishError {
|
||||||
PublishPrepare(PathBuf, #[source] Box<PublishPrepareError>),
|
PublishPrepare(PathBuf, #[source] Box<PublishPrepareError>),
|
||||||
#[error("Failed to publish `{}` to {}", _0.user_display(), _1)]
|
#[error("Failed to publish `{}` to {}", _0.user_display(), _1)]
|
||||||
PublishSend(PathBuf, DisplaySafeUrl, #[source] PublishSendError),
|
PublishSend(PathBuf, DisplaySafeUrl, #[source] PublishSendError),
|
||||||
|
#[error("Unable to publish `{}` to {}", _0.user_display(), _1)]
|
||||||
|
Validate(PathBuf, DisplaySafeUrl, #[source] PublishSendError),
|
||||||
#[error("Failed to obtain token for trusted publishing")]
|
#[error("Failed to obtain token for trusted publishing")]
|
||||||
TrustedPublishing(#[from] TrustedPublishingError),
|
TrustedPublishing(#[from] TrustedPublishingError),
|
||||||
#[error("{0} are not allowed when using trusted publishing")]
|
#[error("{0} are not allowed when using trusted publishing")]
|
||||||
|
|
@ -383,6 +385,7 @@ pub async fn check_trusted_publishing(
|
||||||
/// Implements a custom retry flow since the request isn't cloneable.
|
/// Implements a custom retry flow since the request isn't cloneable.
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
file: &Path,
|
file: &Path,
|
||||||
|
form_metadata: &FormMetadata,
|
||||||
raw_filename: &str,
|
raw_filename: &str,
|
||||||
filename: &DistFilename,
|
filename: &DistFilename,
|
||||||
registry: &DisplaySafeUrl,
|
registry: &DisplaySafeUrl,
|
||||||
|
|
@ -392,23 +395,19 @@ pub async fn upload(
|
||||||
download_concurrency: &Semaphore,
|
download_concurrency: &Semaphore,
|
||||||
reporter: Arc<impl Reporter>,
|
reporter: Arc<impl Reporter>,
|
||||||
) -> Result<bool, PublishError> {
|
) -> Result<bool, PublishError> {
|
||||||
let form_metadata = FormMetadata::read_from_file(file, filename)
|
|
||||||
.await
|
|
||||||
.map_err(|err| PublishError::PublishPrepare(file.to_path_buf(), Box::new(err)))?;
|
|
||||||
|
|
||||||
let mut n_past_retries = 0;
|
let mut n_past_retries = 0;
|
||||||
let start_time = SystemTime::now();
|
let start_time = SystemTime::now();
|
||||||
// N.B. We cannot use the client policy here because it is set to zero retries
|
// N.B. We cannot use the client policy here because it is set to zero retries.
|
||||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(retries_from_env()?);
|
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(retries_from_env()?);
|
||||||
loop {
|
loop {
|
||||||
let (request, idx) = build_request(
|
let (request, idx) = build_upload_request(
|
||||||
file,
|
file,
|
||||||
raw_filename,
|
raw_filename,
|
||||||
filename,
|
filename,
|
||||||
registry,
|
registry,
|
||||||
client,
|
client,
|
||||||
credentials,
|
credentials,
|
||||||
&form_metadata,
|
form_metadata,
|
||||||
reporter.clone(),
|
reporter.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -467,6 +466,51 @@ pub async fn upload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate a file against a registry.
|
||||||
|
pub async fn validate(
|
||||||
|
file: &Path,
|
||||||
|
form_metadata: &FormMetadata,
|
||||||
|
raw_filename: &str,
|
||||||
|
registry: &DisplaySafeUrl,
|
||||||
|
store: &PyxTokenStore,
|
||||||
|
client: &BaseClient,
|
||||||
|
credentials: &Credentials,
|
||||||
|
) -> Result<(), PublishError> {
|
||||||
|
if store.is_known_url(registry) {
|
||||||
|
debug!("Performing validation request for {registry}");
|
||||||
|
|
||||||
|
let mut validation_url = registry.clone();
|
||||||
|
validation_url
|
||||||
|
.path_segments_mut()
|
||||||
|
.expect("URL must have path segments")
|
||||||
|
.push("validate");
|
||||||
|
|
||||||
|
let request = build_validation_request(
|
||||||
|
raw_filename,
|
||||||
|
&validation_url,
|
||||||
|
client,
|
||||||
|
credentials,
|
||||||
|
form_metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = request.send().await.map_err(|err| {
|
||||||
|
PublishError::Validate(
|
||||||
|
file.to_path_buf(),
|
||||||
|
registry.clone(),
|
||||||
|
PublishSendError::ReqwestMiddleware(err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
handle_response(&validation_url, response)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PublishError::Validate(file.to_path_buf(), registry.clone(), err))?;
|
||||||
|
} else {
|
||||||
|
debug!("Skipping validation request for unsupported publish URL: {registry}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Check whether we should skip the upload of a file because it already exists on the index.
|
/// Check whether we should skip the upload of a file because it already exists on the index.
|
||||||
pub async fn check_url(
|
pub async fn check_url(
|
||||||
check_url_client: &CheckUrlClient<'_>,
|
check_url_client: &CheckUrlClient<'_>,
|
||||||
|
|
@ -647,13 +691,13 @@ async fn metadata(file: &Path, filename: &DistFilename) -> Result<Metadata23, Pu
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct FormMetadata(Vec<(&'static str, String)>);
|
pub struct FormMetadata(Vec<(&'static str, String)>);
|
||||||
|
|
||||||
impl FormMetadata {
|
impl FormMetadata {
|
||||||
/// Collect the non-file fields for the multipart request from the package METADATA.
|
/// Collect the non-file fields for the multipart request from the package METADATA.
|
||||||
///
|
///
|
||||||
/// Reference implementation: <https://github.com/pypi/warehouse/blob/d2c36d992cf9168e0518201d998b2707a3ef1e72/warehouse/forklift/legacy.py#L1376-L1430>
|
/// Reference implementation: <https://github.com/pypi/warehouse/blob/d2c36d992cf9168e0518201d998b2707a3ef1e72/warehouse/forklift/legacy.py#L1376-L1430>
|
||||||
async fn read_from_file(
|
pub async fn read_from_file(
|
||||||
file: &Path,
|
file: &Path,
|
||||||
filename: &DistFilename,
|
filename: &DistFilename,
|
||||||
) -> Result<Self, PublishPrepareError> {
|
) -> Result<Self, PublishPrepareError> {
|
||||||
|
|
@ -770,8 +814,8 @@ impl<'a> IntoIterator for &'a FormMetadata {
|
||||||
|
|
||||||
/// Build the upload request.
|
/// Build the upload request.
|
||||||
///
|
///
|
||||||
/// Returns the request and the reporter progress bar id.
|
/// Returns the [`RequestBuilder`] and the reporter progress bar ID.
|
||||||
async fn build_request<'a>(
|
async fn build_upload_request<'a>(
|
||||||
file: &Path,
|
file: &Path,
|
||||||
raw_filename: &str,
|
raw_filename: &str,
|
||||||
filename: &DistFilename,
|
filename: &DistFilename,
|
||||||
|
|
@ -840,6 +884,63 @@ async fn build_request<'a>(
|
||||||
Ok((request, idx))
|
Ok((request, idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the validation request, to validate the upload without actually uploading the file.
|
||||||
|
///
|
||||||
|
/// Returns the [`RequestBuilder`].
|
||||||
|
fn build_validation_request<'a>(
|
||||||
|
raw_filename: &str,
|
||||||
|
registry: &DisplaySafeUrl,
|
||||||
|
client: &'a BaseClient,
|
||||||
|
credentials: &Credentials,
|
||||||
|
form_metadata: &FormMetadata,
|
||||||
|
) -> RequestBuilder<'a> {
|
||||||
|
let mut form = reqwest::multipart::Form::new();
|
||||||
|
for (key, value) in form_metadata.iter() {
|
||||||
|
form = form.text(*key, value.clone());
|
||||||
|
}
|
||||||
|
form = form.text("filename", raw_filename.to_owned());
|
||||||
|
|
||||||
|
// If we have a username but no password, attach the username to the URL so the authentication
|
||||||
|
// middleware can find the matching password.
|
||||||
|
let url = if let Some(username) = credentials
|
||||||
|
.username()
|
||||||
|
.filter(|_| credentials.password().is_none())
|
||||||
|
{
|
||||||
|
let mut url = registry.clone();
|
||||||
|
let _ = url.set_username(username);
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
registry.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut request = client
|
||||||
|
.for_host(&url)
|
||||||
|
.post(Url::from(url))
|
||||||
|
.multipart(form)
|
||||||
|
// Ask PyPI for a structured error messages instead of HTML-markup error messages.
|
||||||
|
// For other registries, we ask them to return plain text over HTML. See
|
||||||
|
// [`PublishSendError::extract_remote_error`].
|
||||||
|
.header(
|
||||||
|
reqwest::header::ACCEPT,
|
||||||
|
"application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7",
|
||||||
|
);
|
||||||
|
|
||||||
|
match credentials {
|
||||||
|
Credentials::Basic { password, .. } => {
|
||||||
|
if password.is_some() {
|
||||||
|
debug!("Using HTTP Basic authentication");
|
||||||
|
request = request.header(AUTHORIZATION, credentials.to_header_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Credentials::Bearer { .. } => {
|
||||||
|
debug!("Using Bearer token authentication");
|
||||||
|
request = request.header(AUTHORIZATION, credentials.to_header_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
/// Log response information and map response to an error variant if not successful.
|
/// Log response information and map response to an error variant if not successful.
|
||||||
async fn handle_response(registry: &Url, response: Response) -> Result<(), PublishSendError> {
|
async fn handle_response(registry: &Url, response: Response) -> Result<(), PublishSendError> {
|
||||||
let status_code = response.status();
|
let status_code = response.status();
|
||||||
|
|
@ -919,7 +1020,7 @@ mod tests {
|
||||||
use uv_distribution_filename::DistFilename;
|
use uv_distribution_filename::DistFilename;
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
|
|
||||||
use crate::{FormMetadata, Reporter, build_request};
|
use crate::{FormMetadata, Reporter, build_upload_request};
|
||||||
|
|
||||||
struct DummyReporter;
|
struct DummyReporter;
|
||||||
|
|
||||||
|
|
@ -998,7 +1099,7 @@ mod tests {
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let client = BaseClientBuilder::default().build();
|
let client = BaseClientBuilder::default().build();
|
||||||
let (request, _) = build_request(
|
let (request, _) = build_upload_request(
|
||||||
&file,
|
&file,
|
||||||
raw_filename,
|
raw_filename,
|
||||||
&filename,
|
&filename,
|
||||||
|
|
@ -1150,7 +1251,7 @@ mod tests {
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
let client = BaseClientBuilder::default().build();
|
let client = BaseClientBuilder::default().build();
|
||||||
let (request, _) = build_request(
|
let (request, _) = build_upload_request(
|
||||||
&file,
|
&file,
|
||||||
raw_filename,
|
raw_filename,
|
||||||
&filename,
|
&filename,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@ use console::Term;
|
||||||
use owo_colors::{AnsiColors, OwoColorize};
|
use owo_colors::{AnsiColors, OwoColorize};
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
use uv_auth::Credentials;
|
use uv_auth::{Credentials, PyxTokenStore};
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder};
|
use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder};
|
||||||
use uv_configuration::{KeyringProviderType, TrustedPublishing};
|
use uv_configuration::{KeyringProviderType, TrustedPublishing};
|
||||||
use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl};
|
use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl};
|
||||||
use uv_publish::{
|
use uv_publish::{
|
||||||
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
|
CheckUrlClient, FormMetadata, PublishError, TrustedPublishResult, check_trusted_publishing,
|
||||||
|
files_for_publishing, upload,
|
||||||
};
|
};
|
||||||
use uv_redacted::DisplaySafeUrl;
|
use uv_redacted::DisplaySafeUrl;
|
||||||
use uv_warnings::{warn_user_once, write_error_chain};
|
use uv_warnings::{warn_user_once, write_error_chain};
|
||||||
|
|
@ -32,6 +33,7 @@ pub(crate) async fn publish(
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
check_url: Option<IndexUrl>,
|
check_url: Option<IndexUrl>,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
dry_run: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
|
@ -42,8 +44,20 @@ pub(crate) async fn publish(
|
||||||
let files = files_for_publishing(paths)?;
|
let files = files_for_publishing(paths)?;
|
||||||
match files.len() {
|
match files.len() {
|
||||||
0 => bail!("No files found to publish"),
|
0 => bail!("No files found to publish"),
|
||||||
1 => writeln!(printer.stderr(), "Publishing 1 file to {publish_url}")?,
|
1 => {
|
||||||
n => writeln!(printer.stderr(), "Publishing {n} files {publish_url}")?,
|
if dry_run {
|
||||||
|
writeln!(printer.stderr(), "Checking 1 file against {publish_url}")?;
|
||||||
|
} else {
|
||||||
|
writeln!(printer.stderr(), "Publishing 1 file to {publish_url}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n => {
|
||||||
|
if dry_run {
|
||||||
|
writeln!(printer.stderr(), "Checking {n} files against {publish_url}")?;
|
||||||
|
} else {
|
||||||
|
writeln!(printer.stderr(), "Publishing {n} files to {publish_url}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// * For the uploads themselves, we roll our own retries due to
|
// * For the uploads themselves, we roll our own retries due to
|
||||||
|
|
@ -115,15 +129,48 @@ pub(crate) async fn publish(
|
||||||
|
|
||||||
let size = fs_err::metadata(&file)?.len();
|
let size = fs_err::metadata(&file)?.len();
|
||||||
let (bytes, unit) = human_readable_bytes(size);
|
let (bytes, unit) = human_readable_bytes(size);
|
||||||
writeln!(
|
if dry_run {
|
||||||
printer.stderr(),
|
writeln!(
|
||||||
"{} {filename} {}",
|
printer.stderr(),
|
||||||
"Uploading".bold().green(),
|
"{} {filename} {}",
|
||||||
format!("({bytes:.1}{unit})").dimmed()
|
"Checking".bold().cyan(),
|
||||||
)?;
|
format!("({bytes:.1}{unit})").dimmed()
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{} {filename} {}",
|
||||||
|
"Uploading".bold().green(),
|
||||||
|
format!("({bytes:.1}{unit})").dimmed()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect the metadata for the file.
|
||||||
|
let form_metadata = FormMetadata::read_from_file(&file, &filename)
|
||||||
|
.await
|
||||||
|
.map_err(|err| PublishError::PublishPrepare(file.clone(), Box::new(err)))?;
|
||||||
|
|
||||||
|
// Run validation checks on the file, but don't upload it (if possible).
|
||||||
|
let store = PyxTokenStore::from_settings()?;
|
||||||
|
uv_publish::validate(
|
||||||
|
&file,
|
||||||
|
&form_metadata,
|
||||||
|
&raw_filename,
|
||||||
|
&publish_url,
|
||||||
|
&store,
|
||||||
|
&upload_client,
|
||||||
|
&credentials,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let reporter = PublishReporter::single(printer);
|
let reporter = PublishReporter::single(printer);
|
||||||
let uploaded = upload(
|
let uploaded = upload(
|
||||||
&file,
|
&file,
|
||||||
|
&form_metadata,
|
||||||
&raw_filename,
|
&raw_filename,
|
||||||
&filename,
|
&filename,
|
||||||
&publish_url,
|
&publish_url,
|
||||||
|
|
@ -136,6 +183,7 @@ pub(crate) async fn publish(
|
||||||
)
|
)
|
||||||
.await?; // Filename and/or URL are already attached, if applicable.
|
.await?; // Filename and/or URL are already attached, if applicable.
|
||||||
info!("Upload succeeded");
|
info!("Upload succeeded");
|
||||||
|
|
||||||
if !uploaded {
|
if !uploaded {
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
|
|
|
||||||
|
|
@ -1645,6 +1645,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
files,
|
files,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
dry_run,
|
||||||
publish_url,
|
publish_url,
|
||||||
trusted_publishing,
|
trusted_publishing,
|
||||||
keyring_provider,
|
keyring_provider,
|
||||||
|
|
@ -1699,6 +1700,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
password,
|
password,
|
||||||
check_url,
|
check_url,
|
||||||
index_locations,
|
index_locations,
|
||||||
|
dry_run,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3414,6 +3414,7 @@ pub(crate) struct PublishSettings {
|
||||||
pub(crate) username: Option<String>,
|
pub(crate) username: Option<String>,
|
||||||
pub(crate) password: Option<String>,
|
pub(crate) password: Option<String>,
|
||||||
pub(crate) index: Option<String>,
|
pub(crate) index: Option<String>,
|
||||||
|
pub(crate) dry_run: bool,
|
||||||
|
|
||||||
// Both CLI and configuration.
|
// Both CLI and configuration.
|
||||||
pub(crate) publish_url: DisplaySafeUrl,
|
pub(crate) publish_url: DisplaySafeUrl,
|
||||||
|
|
@ -3458,6 +3459,7 @@ impl PublishSettings {
|
||||||
files: args.files,
|
files: args.files,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
dry_run: args.dry_run,
|
||||||
publish_url: args
|
publish_url: args
|
||||||
.publish_url
|
.publish_url
|
||||||
.combine(publish_url)
|
.combine(publish_url)
|
||||||
|
|
|
||||||
|
|
@ -5743,6 +5743,8 @@ uv publish [OPTIONS] [FILES]...
|
||||||
<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p></dd><dt id="uv-publish--directory"><a href="#uv-publish--directory"><code>--directory</code></a> <i>directory</i></dt><dd><p>Change to the given directory prior to running the command.</p>
|
<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p></dd><dt id="uv-publish--directory"><a href="#uv-publish--directory"><code>--directory</code></a> <i>directory</i></dt><dd><p>Change to the given directory prior to running the command.</p>
|
||||||
<p>Relative paths are resolved with the given directory as the base.</p>
|
<p>Relative paths are resolved with the given directory as the base.</p>
|
||||||
<p>See <code>--project</code> to only change the project root directory.</p>
|
<p>See <code>--project</code> to only change the project root directory.</p>
|
||||||
|
</dd><dt id="uv-publish--dry-run"><a href="#uv-publish--dry-run"><code>--dry-run</code></a></dt><dd><p>Perform a dry run without uploading files.</p>
|
||||||
|
<p>When enabled, the command will check for existing files if <code>--check-url</code> is provided, and will perform validation against the index if supported, but will not upload any files.</p>
|
||||||
</dd><dt id="uv-publish--help"><a href="#uv-publish--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
</dd><dt id="uv-publish--help"><a href="#uv-publish--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
||||||
</dd><dt id="uv-publish--index"><a href="#uv-publish--index"><code>--index</code></a> <i>index</i></dt><dd><p>The name of an index in the configuration to use for publishing.</p>
|
</dd><dt id="uv-publish--index"><a href="#uv-publish--index"><code>--index</code></a> <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>
|
<p>The index must have a <code>publish-url</code> setting, for example:</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue