1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//! Upload assets to GitHub releases
//!
//! Pattern follows `create_release.rs` - direct async functions without `spawn_task`
use bytes::Bytes;
use octocrab::{Octocrab, models::repos::Asset};
use std::sync::Arc;
/// Options for uploading a release asset
#[derive(Debug, Clone)]
pub struct UploadAssetOptions {
/// Release ID from `create_release`
pub release_id: u64,
/// Asset filename (e.g., "KodegenHelper.app-macos-aarch64.zip")
pub asset_name: String,
/// Optional label for the asset
pub label: Option<String>,
/// File content as bytes
pub content: Bytes,
/// If true, delete existing asset with same name before upload.
/// Default: false (safer - fails if asset exists)
pub replace_existing: bool,
}
/// Upload an asset to a GitHub release using octocrab
///
/// Uses the `release_id` from `create_release` and uploads binary content.
/// Returns the uploaded asset with download URL.
pub async fn upload_release_asset(
client: Arc<Octocrab>,
owner: &str,
repo: &str,
options: UploadAssetOptions,
) -> Result<Asset, octocrab::Error> {
// Step 1: If replace_existing, find and delete existing asset
if options.replace_existing {
// List assets for this release
let assets_page = client
.repos(owner, repo)
.releases()
.assets(options.release_id)
.per_page(100)
.send()
.await?;
// Find asset with matching name
if let Some(existing) = assets_page
.items
.iter()
.find(|a| a.name == options.asset_name)
{
// Delete the existing asset
delete_release_asset(
client.clone(),
owner,
repo,
existing.id.0, // AssetId is a newtype wrapper around u64
)
.await?;
}
// If no match found, that's fine - proceed with upload
}
// Step 2: Upload the new asset
// WORKAROUND: octocrab 0.47 doesn't URL-encode filenames before URI parsing
// Encode the filename ourselves before passing to octocrab
let encoded_name = urlencoding::encode(&options.asset_name).to_string();
let encoded_label = options.label.as_ref().map(|l| urlencoding::encode(l).to_string());
let repos = client.repos(owner, repo);
let releases = repos.releases();
if let Some(label) = encoded_label {
releases
.upload_asset(options.release_id, &encoded_name, options.content)
.label(&label)
.send()
.await
} else {
releases
.upload_asset(options.release_id, &encoded_name, options.content)
.send()
.await
}
}
/// Delete a release asset
pub async fn delete_release_asset(
client: Arc<Octocrab>,
owner: &str,
repo: &str,
asset_id: u64,
) -> Result<(), octocrab::Error> {
client
.repos(owner, repo)
.release_assets()
.delete(asset_id)
.await
}