Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CI] Adding assets.json file to the releases. #9564

Merged
merged 7 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions build/build/release-body.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ The template itself is written in [Handlebars](https://handlebarsjs.com/).
Enso IDE is the main product of the Enso project. The packages are stand-alone, they contain both GUI and the backend.

Download links:

- [Linux]({{download_prefix}}/enso-linux-x86_64-{{version}}.AppImage) (AppImage);
- [macOS (x64)]({{download_prefix}}/enso-mac-x64-{{version}}.dmg) (DMG);
- [macOS (arm64)]({{download_prefix}}/enso-mac-arm64-{{version}}.dmg) (DMG);
- [Windows]({{download_prefix}}/enso-win-x64-{{version}}.exe) (Installer Executable).
{{#each assets.ide}}
- [{{target_pretty}}]({{url}})
{{/each}}

This is the recommended download for most users.

Expand All @@ -27,11 +25,9 @@ This is the recommended download for most users.
If you are interested in using Enso Engine command line tools only, download the Enso Engine bundle.

Download links:

- [Linux]({{download_prefix}}/enso-bundle-{{version}}-linux-amd64.tar.gz);
- [macOS (x64)]({{download_prefix}}/enso-bundle-{{version}}-macos-amd64.tar.gz);
- [macOS (arm64)]({{download_prefix}}/enso-bundle-{{version}}-macos-amd64.tar.gz);
- [Windows]({{download_prefix}}/enso-bundle-{{version}}-windows-amd64.zip).
{{#each assets.engine}}
- [{{target_pretty}}]({{url}})
{{/each}}

These are archives containing the [Enso portable distribution](https://enso.org/docs/developer/enso/distribution/distribution.html#portable-enso-distribution-layout). User is responsible for setting up the environment variables and adding the `bin` directory to the `PATH`.

Expand Down
2 changes: 1 addition & 1 deletion build/build/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl TargetTriple {
/// Get the triple effectively used by the Engine build.
///
/// This might differ from `self` if Engine for some reason needs to cross-compile. Currently
/// this is not the case, previously it was used to force x64 on Applce Silicon.
/// this is not the case, previously it was used to force x64 on Apple Silicon.
pub fn engine(&self) -> Self {
self.clone()
}
Expand Down
34 changes: 20 additions & 14 deletions build/build/src/project/ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,9 @@ impl Artifact {
_ => todo!("{target_os}-{target_arch} combination is not supported"),
}
.into();
// Electron-builder does something like this:
// https://github.com/electron-userland/electron-builder/blob/master/packages/builder-util/src/arch.ts
let arch_string = match (target_os, target_arch) {
(OS::Linux, Arch::X86_64) => "x86_64",
(_, Arch::X86_64) => "x64",
(_, Arch::AArch64) => "arm64",
_ => todo!("{target_os}-{target_arch} combination is not supported"),
};
let image = dist_dir.as_ref().join(match target_os {
OS::Linux => format!("enso-linux-{arch_string}-{version}.AppImage"),
OS::MacOS => format!("enso-mac-{arch_string}-{version}.dmg"),
OS::Windows => format!("enso-win-{arch_string}-{version}.exe"),
_ => todo!("{target_os}-{target_arch} combination is not supported"),
});

let image_filename = electron_image_filename(target_os, target_arch, version);
let image = dist_dir.as_ref().join(image_filename);
Self {
image_checksum: image.with_extension("sha256"),
image,
Expand Down Expand Up @@ -144,3 +132,21 @@ impl Ide {
.boxed()
}
}

/// Filename of the image that electron-builder will produce.
pub fn electron_image_filename(target_os: OS, target_arch: Arch, version: &Version) -> String {
// Electron-builder does something like this:
// https://github.com/electron-userland/electron-builder/blob/master/packages/builder-util/src/arch.ts
let arch_string = match (target_os, target_arch) {
(OS::Linux, Arch::X86_64) => "x86_64",
(_, Arch::X86_64) => "x64",
(_, Arch::AArch64) => "arm64",
_ => todo!("{target_os}-{target_arch} combination is not supported"),
};
match target_os {
OS::Linux => format!("enso-linux-{arch_string}-{version}.AppImage"),
OS::MacOS => format!("enso-mac-{arch_string}-{version}.dmg"),
OS::Windows => format!("enso-win-{arch_string}-{version}.exe"),
_ => todo!("{target_os}-{target_arch} combination is not supported"),
}
}
4 changes: 1 addition & 3 deletions build/build/src/project_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ use crate::prelude::*;

use crate::paths::TargetTriple;

use ide_ci::github::release::ARCHIVE_EXTENSION;



pub fn url(target: &TargetTriple) -> Result<Url> {
Expand All @@ -13,7 +11,7 @@ pub fn url(target: &TargetTriple) -> Result<Url> {
repo = "ci-build",
tag = target.versions.tag(),
asset = format!("project-manager-bundle-{target}"),
ext = ARCHIVE_EXTENSION,
ext = ide_ci::github::release::archive_extension(),
);
Url::parse(&url_text).anyhow_err()
}
Expand Down
107 changes: 97 additions & 10 deletions build/build/src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ use serde_json::json;
use tempfile::tempdir;


// ==============
// === Export ===
// ==============

pub mod manifest;



/// Get the prefix of URL of the release's asset in GitHub.
///
Expand All @@ -36,10 +43,22 @@ use tempfile::tempdir;
/// let repo = RepoRef::new("enso-org", "enso");
/// let version = Version::from_str("2020.1.1").unwrap();
/// let prefix = download_asset_prefix(&repo, &version);
/// assert_eq!(prefix, "https://github.com/enso-org/enso/releases/download/2020.1.1");
/// assert_eq!(prefix.as_str(), "https://github.com/enso-org/enso/releases/download/2020.1.1");
/// ```
pub fn download_asset_prefix(repo: &impl IsRepo, version: &Version) -> String {
format!("https://github.com/{repo}/releases/download/{version}",)
pub fn download_asset_prefix(repo: &impl IsRepo, version: &Version) -> Url {
let text = format!("https://github.com/{repo}/releases/download/{version}",);
Url::from_str(&text).expect("Failed to parse the URL.")
}

/// Get the URL for downloading the asset from the GitHub release.
pub fn download_asset(
repo: &impl IsRepo,
version: &Version,
asset_name: impl AsRef<str>,
) -> String {
let prefix = download_asset_prefix(repo, version);
let asset_name = asset_name.as_ref();
format!("{prefix}/{asset_name}")
}

/// Generate placeholders for the release notes.
Expand All @@ -55,12 +74,11 @@ pub fn release_body_placeholders(
ret.insert("edition", context.triple.versions.edition_name().into());
ret.insert("repo", serde_json::to_value(&context.remote_repo)?);
ret.insert(
"download_prefix",
format!(
"https://github.com/{}/releases/download/{}",
context.remote_repo, context.triple.versions.version
)
.into(),
"assets",
serde_json::to_value(manifest::Assets::new(
&context.remote_repo,
&context.triple.versions.version,
))?,
);

// Generate the release notes.
Expand Down Expand Up @@ -174,6 +192,39 @@ pub async fn publish_release(context: &BuildContext) -> Result {
debug!("Updating edition in the AWS S3.");
crate::aws::update_manifest(&remote_repo, &edition_file_path).await?;

// Add assets manifest.
let manifest = manifest::Assets::new(&remote_repo, &triple.versions.version);
let tempdir = tempdir()?;
let manifest_path = tempdir.path().join(manifest::ASSETS_MANIFEST_FILENAME);
ide_ci::fs::write_json(&manifest_path, &manifest)?;
release_handle.upload_asset_file(&manifest_path).await?;

// The validation step is performed to enable issue reporting and enhance issue visibility.
// Currently, even if the validation fails, the release will not be retracted.
validate_release(release_handle).await?;

Ok(())
}

/// Perform basic check if the release contains advertised assets.
///
/// This should be run only on a published (non-draft) release, as asset download URLs change after
/// publishing.
#[context("Failed to validate release: {release:?}")]
pub async fn validate_release(release: github::release::Handle) -> Result {
let info = release.get().await?;
ensure!(!info.draft, "Release is a draft.");
let version = Version::from_str(&info.tag_name)?;
let manifest_url = download_asset(&release.repo, &version, manifest::ASSETS_MANIFEST_FILENAME);
let manifest = ide_ci::io::download_all(&manifest_url)
.await
.context("Failed to download assets manifest.")?;
let manifest: manifest::Assets =
serde_json::from_slice(&manifest).context("Failed to parse assets manifest.")?;
for asset in manifest.assets() {
let response = reqwest::Client::new().get(&asset.url).send().await?;
ensure!(response.status().is_success(), "Failed to download asset: {}", asset.url);
}
Ok(())
}

Expand Down Expand Up @@ -293,10 +344,11 @@ pub async fn promote_release(context: &BuildContext, version_designation: Design
Ok(())
}


#[cfg(test)]
mod tests {
use super::*;
use ide_ci::cache::Cache;
use ide_ci::github::setup_octocrab;

#[tokio::test]
#[ignore]
Expand All @@ -306,4 +358,39 @@ mod tests {
notify_cloud_about_gui(&version).await?;
Ok(())
}

#[tokio::test]
#[ignore]
async fn release_assets() -> Result {
setup_logging()?;

let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = crate_dir.parent().unwrap().parent().unwrap();
let version = Version::from_str("2024.1.1-nightly.2024.3.26")?;
let triple = TargetTriple::new(Versions::new(version.clone()));
let context = BuildContext {
inner: project::Context {
repo_root: crate::paths::new_repo_root(repo_root, &triple),
octocrab: setup_octocrab().await?,
cache: Cache::new_default().await?,
},
remote_repo: github::Repo::new("enso-org", "enso"),
triple: TargetTriple::new(Versions::new(version.clone())),
};

let release_body = generate_release_body(&context).await?;
debug!("Release body: {}", release_body);

let manifest = manifest::Assets::new(&context.remote_repo, &version);
let manifest_json = serde_json::to_string_pretty(&manifest)?;
debug!("Manifest: {}", manifest_json);

let all_assets = manifest.ide.iter().chain(&manifest.engine);
for asset in all_assets {
let response = reqwest::Client::new().get(&asset.url).send().await?;
ensure!(response.status().is_success(), "Failed to download asset: {}", asset.url);
}

Ok(())
}
}
92 changes: 92 additions & 0 deletions build/build/src/release/manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Description of the release assets.
//!
//! See [Assets].

use crate::prelude::*;

use crate::paths::TargetTriple;
use crate::project;
use crate::release;
use crate::version::Versions;



/// Name of the assets manifest file.
///
/// The website uses this name to find the assets manifest, so it should be kept in sync.
pub const ASSETS_MANIFEST_FILENAME: &str = "assets.json";

/// A platform-specific asset being part of the release, see [Assets] for the purpose.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Asset {
pub os: OS,
pub arch: Arch,
pub url: String,
/// User-friendly description of the target platform.
pub target_pretty: String,
}

impl Asset {
pub fn new(url: String, triple: &TargetTriple) -> Self {
let target_pretty = match (triple.os, triple.arch) {
(OS::Windows, Arch::X86_64) => "Windows".into(),
(OS::Linux, Arch::X86_64) => "Linux".into(),
(OS::MacOS, Arch::X86_64) => "macOS (Intel)".into(),
(OS::MacOS, Arch::AArch64) => "macOS (Apple silicon)".into(),
(os, arch) => format!("{os} {arch}"),
};
Self { os: triple.os, arch: triple.arch, url, target_pretty }
}

/// Description od the asset with IDE image.
pub fn new_ide(repo: &impl IsRepo, triple: &TargetTriple) -> Self {
let filename =
project::ide::electron_image_filename(triple.os, triple.arch, &triple.versions.version);
let url = release::download_asset(repo, &triple.versions.version, filename);
Self::new(url, triple)
}

/// Description od the asset with Engine bundle.
pub fn new_engine(repo: &impl IsRepo, triple: &TargetTriple) -> Self {
use crate::paths::generated::RepoRootBuiltDistributionEnsoBundleTriple;
let stem = RepoRootBuiltDistributionEnsoBundleTriple::segment_name(triple.to_string());
let ext = ide_ci::github::release::archive_extension_for(triple.os);
let filename = format!("{stem}.{ext}");
let url = release::download_asset(repo, &triple.versions.version, filename);
Self::new(url, triple)
}
}


/// Describes the assets that are part of the release.
///
/// The information is used to:
/// * create `assets.json` file used by the project's website,
/// * fill information in the release description template.
// When changing the structure, make sure that it does not break the website.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Assets {
/// IDE packages. The exact format (e.g. installer vs AppImage) depends on the platform.
pub ide: Vec<Asset>,
/// Engine bundles.
pub engine: Vec<Asset>,
/// Version of the release.
pub version: Version,
}

impl Assets {
pub fn new(repo: &impl IsRepo, version: &Version) -> Self {
let mut ret = Self { ide: vec![], engine: vec![], version: version.clone() };
for (os, arch) in crate::ci_gen::RELEASE_TARGETS {
let triple = TargetTriple { os, arch, versions: Versions::new(version.clone()) };
ret.ide.push(Asset::new_ide(repo, &triple));
ret.engine.push(Asset::new_engine(repo, &triple));
}
ret
}

/// Iterate all the assets described in this manifest.
pub fn assets(&self) -> impl Iterator<Item = &Asset> {
self.ide.iter().chain(&self.engine)
}
}
23 changes: 15 additions & 8 deletions build/ci_utils/src/github/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ pub use octocrab::models::ReleaseId as Id;



/// The extensions that will be used for the archives in the GitHub release assets.
/// Extension of the preferred archive format for release assets on the current platform.
pub fn archive_extension() -> &'static str {
archive_extension_for(TARGET_OS)
}

/// Get archive format extension for release assets targeting the given operating system.
///
/// On Windows we use `.zip`, because it has out-of-the-box support in the Explorer.
/// On other platforms we use `.tar.gz`, because it is a good default.
pub const ARCHIVE_EXTENSION: &str = match TARGET_OS {
OS::Windows => "zip",
_ => "tar.gz",
};
/// - For Windows, we use `.zip` because it has built-in support in Windows Explorer.
/// - For all other operating systems, we use `.tar.gz` as the default.
pub fn archive_extension_for(os: OS) -> &'static str {
match os {
OS::Windows => "zip",
_ => "tar.gz",
}
}

/// Types that uniquely identify a release and can be used to fetch it from GitHub.
pub trait IsRelease: Debug {
Expand Down Expand Up @@ -155,7 +162,7 @@ pub trait IsReleaseExt: IsRelease + Sync {
let dir_to_upload = dir_to_upload.as_ref();
let temp_dir = tempfile::tempdir()?;
let archive_path =
custom_name.with_parent(temp_dir.path()).with_appended_extension(ARCHIVE_EXTENSION);
custom_name.with_parent(temp_dir.path()).with_appended_extension(archive_extension());
crate::archive::create(&archive_path, [&dir_to_upload]).await?;
self.upload_asset_file(archive_path).await
}
Expand Down
Loading