mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(downloads): route desktop releases through GitHub
This commit is contained in:
1 parent
dc230926a4
commit
094fb0d1c8
15 files changed
+1093
-155
No files matched your search
+424
-76
@@ -10,7 +10,11 @@ use crate::common::{
|
||||
upload_directory_to_s3, upload_s3_plan_append_only, upload_s3_plan_overwrite,
|
||||
};
|
||||
use crate::functions::write_json_pretty;
|
||||
use crate::release::DESKTOP_RELEASE_ASSET_SUFFIXES;
|
||||
use crate::release::{
|
||||
DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopReleaseAsset, DesktopReleaseDescriptor,
|
||||
desktop_release_descriptor_filename, desktop_release_product,
|
||||
validate_desktop_release_descriptor,
|
||||
};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use aws_sdk_s3::Client as S3Client;
|
||||
use chrono::Utc;
|
||||
@@ -115,6 +119,8 @@ enum DesktopStep {
|
||||
UploadReleaseAssets,
|
||||
DownloadReleaseAssets,
|
||||
UploadPayload,
|
||||
PublishReleaseDescriptor,
|
||||
PublishReleaseMarker,
|
||||
BuildSummary,
|
||||
}
|
||||
|
||||
@@ -243,6 +249,8 @@ pub async fn run(args: BuildDesktopArgs) -> Result<()> {
|
||||
DesktopStep::UploadReleaseAssets => upload_release_assets_step().await,
|
||||
DesktopStep::DownloadReleaseAssets => download_release_assets_step().await,
|
||||
DesktopStep::UploadPayload => upload_payload_step().await,
|
||||
DesktopStep::PublishReleaseDescriptor => publish_release_descriptor_step().await,
|
||||
DesktopStep::PublishReleaseMarker => publish_release_marker_step().await,
|
||||
DesktopStep::BuildSummary => build_summary_step(),
|
||||
}
|
||||
}
|
||||
@@ -1901,12 +1909,23 @@ fn resolve_windows_unpacked_dir(arch: &str, main_exe: &str) -> Result<PathBuf> {
|
||||
struct WindowsPackageConfig {
|
||||
pack_id: &'static str,
|
||||
pack_title: &'static str,
|
||||
artifact_prefix: &'static str,
|
||||
icon_dir: &'static str,
|
||||
runtime: &'static str,
|
||||
main_exe: String,
|
||||
output_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct VelopackAssetIndexEntry {
|
||||
#[serde(rename = "RelativeFileName")]
|
||||
relative_file_name: String,
|
||||
#[serde(rename = "Type")]
|
||||
asset_type: String,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
fn windows_package_config(build_channel: &str, arch: &str) -> WindowsPackageConfig {
|
||||
let canary = build_channel == "canary";
|
||||
let pack_title = if canary { "Fluxer Canary" } else { "Fluxer" };
|
||||
@@ -1917,6 +1936,7 @@ fn windows_package_config(build_channel: &str, arch: &str) -> WindowsPackageConf
|
||||
"fluxer_desktop"
|
||||
},
|
||||
pack_title,
|
||||
artifact_prefix: if canary { "Fluxer-Canary" } else { "Fluxer" },
|
||||
icon_dir: if canary {
|
||||
"icons-canary"
|
||||
} else {
|
||||
@@ -2081,6 +2101,7 @@ fn validate_velopack_output(
|
||||
let full_nupkg = full_nupkg.ok_or_else(|| {
|
||||
anyhow!("Velopack did not produce a full nupkg payload for Windows updates.")
|
||||
})?;
|
||||
let full_nupkg = rename_windows_update_package(config, version, arch, &full_nupkg)?;
|
||||
let release_feed = fs::read_to_string(&legacy_releases)
|
||||
.with_context(|| format!("Failed to read {}", legacy_releases.display()))?;
|
||||
let nupkg_name = file_name_string(&full_nupkg)?;
|
||||
@@ -2096,7 +2117,7 @@ fn validate_velopack_output(
|
||||
config.output_dir.display()
|
||||
)
|
||||
})?;
|
||||
let desired_setup_name = format!("{}-{version}-win-{arch}.exe", config.pack_title);
|
||||
let desired_setup_name = format!("{}-{version}-win-{arch}.exe", config.artifact_prefix);
|
||||
if file_name_string(&setup_exe)? != desired_setup_name {
|
||||
fs::rename(&setup_exe, config.output_dir.join(desired_setup_name))
|
||||
.with_context(|| format!("Failed to rename {}", setup_exe.display()))?;
|
||||
@@ -2104,6 +2125,122 @@ fn validate_velopack_output(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rename_windows_update_package(
|
||||
config: &WindowsPackageConfig,
|
||||
version: &str,
|
||||
arch: &str,
|
||||
source: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
let source_name = file_name_string(source)?;
|
||||
let target_name = format!("{}-{version}-win-{arch}-full.nupkg", config.artifact_prefix);
|
||||
let setup_name = format!("{}-{version}-win-{arch}.exe", config.artifact_prefix);
|
||||
let portable_name = format!(
|
||||
"{}-{version}-portable-win-{arch}.zip",
|
||||
config.artifact_prefix
|
||||
);
|
||||
ensure!(
|
||||
source_name != target_name,
|
||||
"Velopack unexpectedly emitted the canonical package name {target_name:?} before feed normalization"
|
||||
);
|
||||
let legacy_path = config.output_dir.join("RELEASES");
|
||||
let legacy = fs::read_to_string(&legacy_path)
|
||||
.with_context(|| format!("Failed to read {}", legacy_path.display()))?;
|
||||
ensure!(
|
||||
legacy.matches(&source_name).count() == 1,
|
||||
"{} must reference Velopack package {source_name:?} exactly once",
|
||||
legacy_path.display()
|
||||
);
|
||||
fs::write(&legacy_path, legacy.replace(&source_name, &target_name))
|
||||
.with_context(|| format!("Failed to rewrite {}", legacy_path.display()))?;
|
||||
|
||||
let releases_path = config.output_dir.join("releases.win.json");
|
||||
let mut releases: Value = serde_json::from_slice(
|
||||
&fs::read(&releases_path)
|
||||
.with_context(|| format!("Failed to read {}", releases_path.display()))?,
|
||||
)
|
||||
.with_context(|| format!("Failed to parse {}", releases_path.display()))?;
|
||||
let replacements = replace_json_string(&mut releases, &source_name, &target_name);
|
||||
ensure!(
|
||||
replacements == 1,
|
||||
"{} must reference Velopack package {source_name:?} exactly once, found {replacements}",
|
||||
releases_path.display()
|
||||
);
|
||||
write_json_pretty(&releases_path, &releases)?;
|
||||
|
||||
let assets_path = config.output_dir.join("assets.win.json");
|
||||
let mut assets: Vec<VelopackAssetIndexEntry> = serde_json::from_slice(
|
||||
&fs::read(&assets_path)
|
||||
.with_context(|| format!("Failed to read {}", assets_path.display()))?,
|
||||
)
|
||||
.with_context(|| format!("Failed to parse {}", assets_path.display()))?;
|
||||
ensure!(
|
||||
assets.len() == 3,
|
||||
"{} must contain exactly three Velopack assets, found {}",
|
||||
assets_path.display(),
|
||||
assets.len()
|
||||
);
|
||||
let mut asset_types = BTreeSet::new();
|
||||
for asset in &mut assets {
|
||||
ensure!(
|
||||
asset_types.insert(asset.asset_type.as_str()),
|
||||
"{} contains duplicate asset type {:?}",
|
||||
assets_path.display(),
|
||||
asset.asset_type
|
||||
);
|
||||
asset.relative_file_name = match asset.asset_type.as_str() {
|
||||
"Installer" => setup_name.clone(),
|
||||
"Portable" => portable_name.clone(),
|
||||
"Full" => {
|
||||
ensure!(
|
||||
asset.relative_file_name == source_name,
|
||||
"{} Full asset references {:?}, expected {source_name:?}",
|
||||
assets_path.display(),
|
||||
asset.relative_file_name
|
||||
);
|
||||
target_name.clone()
|
||||
}
|
||||
other => bail!(
|
||||
"{} contains unsupported Velopack asset type {other:?}",
|
||||
assets_path.display()
|
||||
),
|
||||
};
|
||||
}
|
||||
ensure!(
|
||||
asset_types == BTreeSet::from(["Full", "Installer", "Portable"]),
|
||||
"{} contains an incomplete Velopack asset inventory",
|
||||
assets_path.display()
|
||||
);
|
||||
write_json_pretty(&assets_path, &assets)?;
|
||||
|
||||
let target = config.output_dir.join(&target_name);
|
||||
fs::rename(source, &target).with_context(|| {
|
||||
format!(
|
||||
"Failed to rename {} to {}",
|
||||
source.display(),
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
fn replace_json_string(value: &mut Value, source: &str, target: &str) -> usize {
|
||||
match value {
|
||||
Value::String(current) if current == source => {
|
||||
*current = target.to_string();
|
||||
1
|
||||
}
|
||||
Value::Array(values) => values
|
||||
.iter_mut()
|
||||
.map(|value| replace_json_string(value, source, target))
|
||||
.sum(),
|
||||
Value::Object(values) => values
|
||||
.values_mut()
|
||||
.map(|value| replace_json_string(value, source, target))
|
||||
.sum(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_windows_unpacked_app(arch: &str, main_exe: &str) -> Option<PathBuf> {
|
||||
windows_unpacked_candidates(arch)
|
||||
.into_iter()
|
||||
@@ -2225,7 +2362,10 @@ fn create_portable_zip_windows_step() -> Result<()> {
|
||||
let portable_marker = pack_dir.join(".portable");
|
||||
fs::write(&portable_marker, "")
|
||||
.with_context(|| format!("Failed to write {}", portable_marker.display()))?;
|
||||
let zip_name = format!("{}-{version}-portable-win-{arch}.zip", config.pack_title);
|
||||
let zip_name = format!(
|
||||
"{}-{version}-portable-win-{arch}.zip",
|
||||
config.artifact_prefix
|
||||
);
|
||||
let zip_path = PathBuf::from("dist-electron").join(zip_name);
|
||||
create_zip_from_dir(&pack_dir, &zip_path)?;
|
||||
remove_file_if_exists(&portable_marker)?;
|
||||
@@ -3159,9 +3299,10 @@ fn verify_windows_signed_artifacts_step() -> Result<()> {
|
||||
config.output_dir.display()
|
||||
)
|
||||
})?;
|
||||
let setup_exe = config
|
||||
.output_dir
|
||||
.join(format!("{}-{version}-win-{arch}.exe", config.pack_title));
|
||||
let setup_exe = config.output_dir.join(format!(
|
||||
"{}-{version}-win-{arch}.exe",
|
||||
config.artifact_prefix
|
||||
));
|
||||
ensure!(
|
||||
setup_exe.is_file(),
|
||||
"Velopack Setup.exe not found: {}",
|
||||
@@ -3169,7 +3310,7 @@ fn verify_windows_signed_artifacts_step() -> Result<()> {
|
||||
);
|
||||
let portable_zip = PathBuf::from("dist-electron").join(format!(
|
||||
"{}-{version}-portable-win-{arch}.zip",
|
||||
config.pack_title
|
||||
config.artifact_prefix
|
||||
));
|
||||
ensure!(
|
||||
portable_zip.is_file(),
|
||||
@@ -3507,100 +3648,243 @@ fn build_payload_step() -> Result<()> {
|
||||
fn prepare_release_assets_step() -> Result<()> {
|
||||
let channel = require_env("CHANNEL")?;
|
||||
let version = require_env("VERSION")?;
|
||||
let product = match channel.as_str() {
|
||||
"stable" => "Fluxer",
|
||||
"canary" => "Fluxer.Canary",
|
||||
other => bail!("Unsupported desktop release channel {other:?}"),
|
||||
};
|
||||
let expected_asset_names = DESKTOP_RELEASE_ASSET_SUFFIXES
|
||||
.iter()
|
||||
.map(|suffix| format!("{product}-{version}-{suffix}"))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let artifacts = Path::new("artifacts");
|
||||
let source_sha = require_env("SOURCE_SHA")?;
|
||||
let s3_prefix = require_env("S3_DESKTOP_PREFIX")?;
|
||||
ensure!(
|
||||
s3_prefix == "desktop",
|
||||
"GitHub desktop releases require S3_DESKTOP_PREFIX=desktop, received {s3_prefix:?}"
|
||||
);
|
||||
let product = desktop_release_product(&channel)?;
|
||||
let payload_root = Path::new("s3_payload").join(&s3_prefix).join(&channel);
|
||||
let release_assets = Path::new("release_assets");
|
||||
remove_dir_if_exists(release_assets)?;
|
||||
fs::create_dir_all(release_assets)?;
|
||||
|
||||
let mut asset_names = BTreeSet::new();
|
||||
for (dir, identity) in payload_artifact_dirs(artifacts, &channel)? {
|
||||
let mut release_builder =
|
||||
DesktopReleaseAssetBuilder::new(&s3_prefix, &channel, &version, product, release_assets);
|
||||
for (platform, arch) in [
|
||||
("win32", "x64"),
|
||||
("win32", "arm64"),
|
||||
("darwin", "x64"),
|
||||
("darwin", "arm64"),
|
||||
("linux", "x64"),
|
||||
("linux", "arm64"),
|
||||
] {
|
||||
let dir = payload_root.join(platform).join(arch);
|
||||
ensure!(
|
||||
identity.desktop_variant == DEFAULT_DESKTOP_VARIANT,
|
||||
"GitHub release asset naming is undefined for desktop variant {:?}",
|
||||
identity.desktop_variant
|
||||
dir.is_dir(),
|
||||
"Desktop release payload directory is missing: {}",
|
||||
dir.display()
|
||||
);
|
||||
let platform = match identity.platform.as_str() {
|
||||
"windows" => "win32",
|
||||
"macos" => "darwin",
|
||||
let manifest_path = dir.join("manifest.json");
|
||||
let manifest: DesktopManifest = serde_json::from_slice(
|
||||
&fs::read(&manifest_path)
|
||||
.with_context(|| format!("Failed to read {}", manifest_path.display()))?,
|
||||
)
|
||||
.with_context(|| format!("Failed to parse {}", manifest_path.display()))?;
|
||||
ensure!(
|
||||
manifest.channel == channel
|
||||
&& manifest.platform == platform
|
||||
&& manifest.arch == arch
|
||||
&& manifest.version == version
|
||||
&& manifest.variant.is_none(),
|
||||
"Desktop release manifest identity mismatch in {}",
|
||||
manifest_path.display()
|
||||
);
|
||||
let expected_kinds = match platform {
|
||||
"win32" => BTreeSet::from(["portable", "setup"]),
|
||||
"darwin" => BTreeSet::from(["dmg", "zip"]),
|
||||
"linux" => BTreeSet::from(["appimage", "deb", "rpm", "tar_gz"]),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let actual_kinds = manifest
|
||||
.files
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<BTreeSet<_>>();
|
||||
ensure!(
|
||||
actual_kinds == expected_kinds,
|
||||
"Incomplete shipped artifact set for {platform}/{arch}: expected {:?}, found {:?}",
|
||||
expected_kinds,
|
||||
actual_kinds
|
||||
);
|
||||
for entry in manifest.files.values() {
|
||||
release_builder.add(platform, arch, &dir.join(entry.filename()), false)?;
|
||||
}
|
||||
let updater_files = desktop_updater_release_files(&dir, platform)?;
|
||||
for updater_file in updater_files {
|
||||
release_builder.add(platform, arch, &updater_file, true)?;
|
||||
}
|
||||
}
|
||||
let mut descriptor_assets = release_builder.finish();
|
||||
descriptor_assets.sort_by(|left, right| left.storage_key.cmp(&right.storage_key));
|
||||
let descriptor = DesktopReleaseDescriptor {
|
||||
schema_version: DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION,
|
||||
channel: channel.clone(),
|
||||
version: version.clone(),
|
||||
release_tag: format!("fluxer-desktop-{channel}@{version}"),
|
||||
source_sha,
|
||||
assets: descriptor_assets,
|
||||
};
|
||||
validate_desktop_release_descriptor(&descriptor, &channel, &version, &descriptor.source_sha)?;
|
||||
let descriptor_path =
|
||||
release_assets.join(desktop_release_descriptor_filename(&channel, &version)?);
|
||||
write_json_pretty(&descriptor_path, &descriptor)?;
|
||||
println!("GitHub release asset tree:");
|
||||
print_tree(release_assets, 2)
|
||||
}
|
||||
|
||||
fn desktop_updater_release_files(dir: &Path, platform: &str) -> Result<Vec<PathBuf>> {
|
||||
let mut files = match platform {
|
||||
"win32" => vec![
|
||||
dir.join("RELEASES"),
|
||||
dir.join("releases.win.json"),
|
||||
dir.join("assets.win.json"),
|
||||
],
|
||||
"darwin" => vec![dir.join("RELEASES.json"), dir.join("releases.json")],
|
||||
"linux" => Vec::new(),
|
||||
other => bail!("Unsupported desktop release platform {other:?}"),
|
||||
};
|
||||
if platform == "win32" {
|
||||
let nupkgs = collect_files(dir)?
|
||||
.into_iter()
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| name.ends_with("-full.nupkg"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ensure!(
|
||||
nupkgs.len() == 1,
|
||||
"Expected one Windows full update package in {}, found {}",
|
||||
dir.display(),
|
||||
nupkgs.len()
|
||||
);
|
||||
files.push(nupkgs[0].clone());
|
||||
}
|
||||
for path in &files {
|
||||
ensure!(
|
||||
path.is_file(),
|
||||
"Desktop updater release file is missing: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
struct DesktopReleaseAssetBuilder<'a> {
|
||||
s3_prefix: &'a str,
|
||||
channel: &'a str,
|
||||
version: &'a str,
|
||||
product: &'a str,
|
||||
release_assets: &'a Path,
|
||||
descriptor_assets: Vec<DesktopReleaseAsset>,
|
||||
storage_keys: BTreeSet<String>,
|
||||
release_asset_content: BTreeMap<String, (String, u64)>,
|
||||
}
|
||||
|
||||
impl<'a> DesktopReleaseAssetBuilder<'a> {
|
||||
fn new(
|
||||
s3_prefix: &'a str,
|
||||
channel: &'a str,
|
||||
version: &'a str,
|
||||
product: &'a str,
|
||||
release_assets: &'a Path,
|
||||
) -> Self {
|
||||
Self {
|
||||
s3_prefix,
|
||||
channel,
|
||||
version,
|
||||
product,
|
||||
release_assets,
|
||||
descriptor_assets: Vec::new(),
|
||||
storage_keys: BTreeSet::new(),
|
||||
release_asset_content: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, platform: &str, arch: &str, source: &Path, qualify_name: bool) -> Result<()> {
|
||||
ensure!(
|
||||
source.is_file(),
|
||||
"Release source is missing: {}",
|
||||
source.display()
|
||||
);
|
||||
let source_name = file_name_string(source)?;
|
||||
let platform_token = match platform {
|
||||
"win32" => "win",
|
||||
"darwin" => "mac",
|
||||
"linux" => "linux",
|
||||
other => bail!("Unsupported desktop release platform {other:?}"),
|
||||
};
|
||||
let candidates = manifest_candidates(&dir, platform, &identity.arch)?;
|
||||
let candidate_kinds = candidates
|
||||
.iter()
|
||||
.map(|(kind, _)| kind.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let expected_kinds = match platform {
|
||||
"win32" => vec!["setup", "portable"],
|
||||
"darwin" => vec!["dmg", "zip"],
|
||||
"linux" => vec!["appimage", "deb", "rpm", "tar_gz"],
|
||||
_ => unreachable!(),
|
||||
let canonical_prefix = format!("{}-{}-", self.product, self.version);
|
||||
let release_asset = if qualify_name && !source_name.starts_with(&canonical_prefix) {
|
||||
format!(
|
||||
"{}-{}-{platform_token}-{arch}-{source_name}",
|
||||
self.product, self.version
|
||||
)
|
||||
} else {
|
||||
source_name.clone()
|
||||
};
|
||||
ensure!(
|
||||
candidate_kinds == expected_kinds,
|
||||
"Incomplete shipped artifact set for {}/{:?}: expected {:?}, found {:?}",
|
||||
identity.platform,
|
||||
identity.arch,
|
||||
expected_kinds,
|
||||
candidate_kinds
|
||||
release_asset.starts_with(&canonical_prefix)
|
||||
&& release_asset.bytes().all(|byte| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')
|
||||
}),
|
||||
"Desktop release asset name is not canonical and URL-safe: {release_asset:?}"
|
||||
);
|
||||
|
||||
for (_, source) in candidates {
|
||||
let source_name = file_name_string(&source)?;
|
||||
let asset_name = clean_release_asset_name(&source_name)?;
|
||||
let storage_key = format!(
|
||||
"{}/{}/{platform}/{arch}/{source_name}",
|
||||
self.s3_prefix, self.channel
|
||||
);
|
||||
ensure!(
|
||||
self.storage_keys.insert(storage_key.clone()),
|
||||
"Duplicate desktop release storage key {storage_key:?}"
|
||||
);
|
||||
let size = fs::metadata(source)
|
||||
.with_context(|| format!("Failed to inspect {}", source.display()))?
|
||||
.len();
|
||||
ensure!(
|
||||
size > 0,
|
||||
"Desktop release source is empty: {}",
|
||||
source.display()
|
||||
);
|
||||
let sha256 = sha256_file(source)?;
|
||||
if let Some((existing_sha256, existing_size)) =
|
||||
self.release_asset_content.get(&release_asset)
|
||||
{
|
||||
ensure!(
|
||||
asset_names.insert(asset_name.clone()),
|
||||
"Duplicate GitHub release asset name {asset_name:?}"
|
||||
existing_sha256 == &sha256 && *existing_size == size,
|
||||
"Desktop release asset {release_asset:?} has conflicting source content"
|
||||
);
|
||||
let destination = release_assets.join(&asset_name);
|
||||
let copied = fs::copy(&source, &destination).with_context(|| {
|
||||
} else {
|
||||
let destination = self.release_assets.join(&release_asset);
|
||||
let copied = fs::copy(source, &destination).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy shipped artifact {} to {}",
|
||||
"Failed to copy desktop release asset {} to {}",
|
||||
source.display(),
|
||||
destination.display()
|
||||
)
|
||||
})?;
|
||||
ensure!(
|
||||
copied > 0,
|
||||
"Copied empty GitHub release asset {}",
|
||||
copied == size,
|
||||
"Desktop release asset copy size mismatch for {}",
|
||||
destination.display()
|
||||
);
|
||||
self.release_asset_content
|
||||
.insert(release_asset.clone(), (sha256.clone(), size));
|
||||
}
|
||||
self.descriptor_assets.push(DesktopReleaseAsset {
|
||||
storage_key,
|
||||
release_asset,
|
||||
sha256,
|
||||
size,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
ensure!(
|
||||
asset_names == expected_asset_names,
|
||||
"GitHub release asset inventory mismatch: expected {expected_asset_names:?}, found {asset_names:?}"
|
||||
);
|
||||
println!("GitHub release asset tree:");
|
||||
print_tree(release_assets, 2)
|
||||
}
|
||||
|
||||
fn clean_release_asset_name(source_name: &str) -> Result<String> {
|
||||
let name = source_name
|
||||
.chars()
|
||||
.map(|character| {
|
||||
if character.is_ascii_whitespace() {
|
||||
'.'
|
||||
} else {
|
||||
character
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
ensure!(
|
||||
name.bytes()
|
||||
.all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_') }),
|
||||
"Cannot produce a clean GitHub release asset name from {source_name:?}"
|
||||
);
|
||||
Ok(name)
|
||||
fn finish(self) -> Vec<DesktopReleaseAsset> {
|
||||
self.descriptor_assets
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_release_assets_step() -> Result<()> {
|
||||
@@ -3922,6 +4206,70 @@ async fn upload_payload_step() -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
fn read_release_descriptor_from_assets() -> Result<(PathBuf, DesktopReleaseDescriptor)> {
|
||||
let channel = require_env("CHANNEL")?;
|
||||
let version = require_env("VERSION")?;
|
||||
let source_sha = require_env("SOURCE_SHA")?;
|
||||
let path =
|
||||
Path::new("release_assets").join(desktop_release_descriptor_filename(&channel, &version)?);
|
||||
let descriptor: DesktopReleaseDescriptor = serde_json::from_slice(
|
||||
&fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?,
|
||||
)
|
||||
.with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
validate_desktop_release_descriptor(&descriptor, &channel, &version, &source_sha)?;
|
||||
Ok((path, descriptor))
|
||||
}
|
||||
|
||||
async fn publish_release_descriptor_step() -> Result<()> {
|
||||
let (descriptor_path, descriptor) = read_release_descriptor_from_assets()?;
|
||||
let client = s3_client(None).await?;
|
||||
let bucket = require_env("S3_BUCKET")?;
|
||||
let key = format!(
|
||||
"desktop/{}/github-releases/{}.json",
|
||||
descriptor.channel, descriptor.version
|
||||
);
|
||||
let plan = vec![
|
||||
S3UploadPlanItem::new(descriptor_path, key)
|
||||
.with_content_type("application/json; charset=utf-8")
|
||||
.with_cache_control(VERSIONED_ARTIFACT_CACHE_CONTROL),
|
||||
];
|
||||
upload_s3_plan_append_only(&client, &bucket, plan).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_release_marker_step() -> Result<()> {
|
||||
let (descriptor_path, descriptor) = read_release_descriptor_from_assets()?;
|
||||
let descriptor_sha256 = sha256_file(&descriptor_path)?;
|
||||
let temp = TempDir::new().context("Failed to create desktop release marker temp directory")?;
|
||||
let marker_path = temp
|
||||
.path()
|
||||
.join(format!("{}.ready.json", descriptor.version));
|
||||
write_json_pretty(
|
||||
&marker_path,
|
||||
&json!({
|
||||
"schema_version": 1,
|
||||
"channel": descriptor.channel,
|
||||
"version": descriptor.version,
|
||||
"release_tag": descriptor.release_tag,
|
||||
"source_sha": descriptor.source_sha,
|
||||
"descriptor_sha256": descriptor_sha256,
|
||||
}),
|
||||
)?;
|
||||
let client = s3_client(None).await?;
|
||||
let bucket = require_env("S3_BUCKET")?;
|
||||
let key = format!(
|
||||
"desktop/{}/github-releases/{}.ready.json",
|
||||
descriptor.channel, descriptor.version
|
||||
);
|
||||
let plan = vec![
|
||||
S3UploadPlanItem::new(marker_path, key)
|
||||
.with_content_type("application/json; charset=utf-8")
|
||||
.with_cache_control(VERSIONED_ARTIFACT_CACHE_CONTROL),
|
||||
];
|
||||
upload_s3_plan_append_only(&client, &bucket, plan).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_payload_directory<F>(
|
||||
client: &S3Client,
|
||||
bucket: &str,
|
||||
|
||||
Reference in new issue
Block a user