Skip to content

Commit 1ee4656

Browse files
authored
fix(release): publish intentional assetless drafts (#10599)
1 parent 722fec9 commit 1ee4656

3 files changed

Lines changed: 33 additions & 23 deletions

File tree

crates/homeboy-release/src/release/executor/github_release/delivery.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,12 @@ pub(crate) enum ExistingReleaseAction {
2424
/// Published release and nothing to attach in this run: a genuine
2525
/// idempotent no-op.
2626
AlreadyPublished,
27-
/// Unpublished Draft carrying assets, with nothing further to attach. The
28-
/// artifacts were already uploaded (by this pipeline's earlier publish
29-
/// stage or a previous attempt); publishing is the only remaining action
30-
/// that makes the pushed tag deliverable.
27+
/// Unpublished Draft ready to publish, either because its assets were
28+
/// already uploaded or because the component does not declare an artifact.
3129
PublishDraft,
32-
/// Unpublished Draft with no assets, and nothing to attach. Publishing here
33-
/// would mark an empty release `latest` and point every downloader at a
34-
/// release with no binaries — strictly worse than the Draft. Fail loudly
35-
/// instead and hand the operator the repair commands.
30+
/// Unpublished Draft for an artifact-expecting component with no assets and
31+
/// nothing to attach. Publishing here would mark an incomplete release
32+
/// `latest`, so fail loudly and hand the operator the repair commands.
3633
EmptyDraft,
3734
/// This run carries artifacts. Reconcile and verify the assets first; the
3835
/// publish decision is made only after verification succeeds.
@@ -42,19 +39,21 @@ pub(crate) enum ExistingReleaseAction {
4239
/// Classify an already-existing GitHub Release.
4340
///
4441
/// `has_artifacts` is whether *this run* resolved release artifacts to attach;
45-
/// `existing_asset_count` is how many assets the release already carries.
42+
/// `existing_asset_count` is how many assets the release already carries;
43+
/// `expects_artifacts` reflects whether the component declares a build artifact.
4644
pub(crate) fn existing_release_action(
4745
is_draft: bool,
4846
has_artifacts: bool,
4947
existing_asset_count: usize,
48+
expects_artifacts: bool,
5049
) -> ExistingReleaseAction {
5150
if has_artifacts {
5251
return ExistingReleaseAction::ReconcileAssets;
5352
}
5453
if !is_draft {
5554
return ExistingReleaseAction::AlreadyPublished;
5655
}
57-
if existing_asset_count == 0 {
56+
if existing_asset_count == 0 && expects_artifacts {
5857
return ExistingReleaseAction::EmptyDraft;
5958
}
6059
ExistingReleaseAction::PublishDraft

crates/homeboy-release/src/release/executor/github_release/run.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,12 @@ pub(crate) fn run_github_release(
279279
));
280280
}
281281

282-
match existing_release_action(metadata.is_draft, has_artifacts, metadata.assets.len()) {
282+
match existing_release_action(
283+
metadata.is_draft,
284+
has_artifacts,
285+
metadata.assets.len(),
286+
component.build_artifact.is_some(),
287+
) {
283288
ExistingReleaseAction::AlreadyPublished => {
284289
homeboy_core::log_status!(
285290
"release",
@@ -295,10 +300,8 @@ pub(crate) fn run_github_release(
295300
));
296301
}
297302
ExistingReleaseAction::EmptyDraft => {
298-
// Publishing an assetless draft would make it `latest` and send
299-
// every downloader (and the Homebrew formula) to a release with
300-
// no binaries. That is worse than leaving the draft for the
301-
// recovery path, which can still upload artifacts into it.
303+
// The component declares a downloadable artifact, so publishing
304+
// its empty draft would make an incomplete release `latest`.
302305
let repair = repair_commands(None, None);
303306
let detail = format!(
304307
"GitHub Release {} for {} is an unpublished draft with no assets, and this run has no artifacts to attach. Refusing to publish an empty release over the pushed tag.",

crates/homeboy-release/src/release/executor/github_release/tests/delivery_tests.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use super::super::{existing_release_action, ExistingReleaseAction};
2020
#[test]
2121
fn published_release_with_nothing_to_attach_is_an_idempotent_no_op() {
2222
assert_eq!(
23-
existing_release_action(false, false, 13),
23+
existing_release_action(false, false, 13, true),
2424
ExistingReleaseAction::AlreadyPublished
2525
);
2626
}
@@ -31,7 +31,7 @@ fn published_release_with_no_assets_is_still_an_idempotent_no_op() {
3131
// assets is a delivery problem for `verify-published` to report, not a
3232
// reason for this step to re-publish something already public.
3333
assert_eq!(
34-
existing_release_action(false, false, 0),
34+
existing_release_action(false, false, 0, true),
3535
ExistingReleaseAction::AlreadyPublished
3636
);
3737
}
@@ -42,27 +42,35 @@ fn stranded_draft_carrying_assets_is_published_not_skipped() {
4242
// already uploaded every asset, and only the un-draft edit is missing.
4343
// Reporting `skipped` here is what let those tags sit undeliverable.
4444
assert_eq!(
45-
existing_release_action(true, false, 15),
45+
existing_release_action(true, false, 15, true),
4646
ExistingReleaseAction::PublishDraft
4747
);
4848
assert_eq!(
49-
existing_release_action(true, false, 1),
49+
existing_release_action(true, false, 1, true),
5050
ExistingReleaseAction::PublishDraft
5151
);
5252
}
5353

5454
#[test]
55-
fn empty_draft_is_refused_rather_than_published() {
55+
fn empty_draft_for_artifact_component_is_refused_rather_than_published() {
5656
// Publishing an assetless draft marks it `latest` and points every
5757
// downloader (and the Homebrew formula) at a release with no binaries.
5858
// That is strictly worse than leaving the draft for the recovery path,
5959
// which can still upload artifacts into it.
6060
assert_eq!(
61-
existing_release_action(true, false, 0),
61+
existing_release_action(true, false, 0, true),
6262
ExistingReleaseAction::EmptyDraft
6363
);
6464
}
6565

66+
#[test]
67+
fn empty_draft_for_assetless_component_is_published() {
68+
assert_eq!(
69+
existing_release_action(true, false, 0, false),
70+
ExistingReleaseAction::PublishDraft
71+
);
72+
}
73+
6674
#[test]
6775
fn a_run_carrying_artifacts_always_reconciles_before_any_publish_decision() {
6876
// Assets must be uploaded and verified by digest before the publish
@@ -71,7 +79,7 @@ fn a_run_carrying_artifacts_always_reconciles_before_any_publish_decision() {
7179
for is_draft in [true, false] {
7280
for existing in [0usize, 1, 15] {
7381
assert_eq!(
74-
existing_release_action(is_draft, true, existing),
82+
existing_release_action(is_draft, true, existing, true),
7583
ExistingReleaseAction::ReconcileAssets,
7684
"is_draft={is_draft} existing_assets={existing}"
7785
);
@@ -86,7 +94,7 @@ fn no_input_combination_reports_a_draft_as_already_published() {
8694
for existing in [0usize, 1, 13, 15] {
8795
for has_artifacts in [true, false] {
8896
assert_ne!(
89-
existing_release_action(true, has_artifacts, existing),
97+
existing_release_action(true, has_artifacts, existing, true),
9098
ExistingReleaseAction::AlreadyPublished,
9199
"a draft must never classify as published (has_artifacts={has_artifacts}, existing_assets={existing})"
92100
);

0 commit comments

Comments
 (0)