From ff713a3dc184e20bc43972be07160c284681282b Mon Sep 17 00:00:00 2001 From: MobiusCraftFlip Date: Mon, 12 Jun 2023 17:54:19 +0100 Subject: [PATCH 01/13] add org-membership publish --- wally-registry-backend/src/auth.rs | 106 ++++++++++++++++++++++++----- wally-registry-backend/src/main.rs | 18 +++-- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/wally-registry-backend/src/auth.rs b/wally-registry-backend/src/auth.rs index dcc3dfd1..59039281 100644 --- a/wally-registry-backend/src/auth.rs +++ b/wally-registry-backend/src/auth.rs @@ -23,10 +23,28 @@ pub enum AuthMode { Unauthenticated, } -#[derive(Deserialize)] +#[derive(Deserialize, Clone, Debug)] pub struct GithubInfo { login: String, id: u64, + organizations_url: String, +} + +#[derive(Deserialize, Debug)] +pub struct GithubOrgInfoOrganization { + login: String, +} + +#[derive(Deserialize, Debug)] +pub struct GithubOrgInfo { + organization: GithubOrgInfoOrganization, + user: GithubInfo, +} + +#[derive(Debug)] +pub struct GithubWriteAccessInfo { + pub user: GithubInfo, + pub organizations: Vec, } impl GithubInfo { @@ -80,26 +98,68 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome { return format_err!(err).status(Status::InternalServerError).into(); } - Ok(response) => response.json::().await, + Ok(response) => response.json::>().await, }; - match github_info { + match github_org_info { + Ok(github_org_info) => { + match github_org_info.get(0) { + Some(org) => { + return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { + user: org.user.clone(), + organizations: github_org_info + .iter() + .map(|x| x.organization.login.to_lowercase()) + .collect::>(), + })); + } + None => { + // The user is in no orgs we can see so we cannot get their userinfo from that. + let response = client + .get("https://api.github.com/user") + .header("accept", "application/json") + .header("user-agent", "wally") + .bearer_auth(&token) + .send() + .await; + + let github_info = match response { + Err(err) => { + return format_err!(err).status(Status::InternalServerError).into(); + } + Ok(response) => response.json::().await, + }; + + match github_info { + Err(err) => format_err!("Github auth failed: {}", err) + .status(Status::Unauthorized) + .into(), + Ok(github_info) => { + return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { + user: github_info, + organizations: vec![], + })); + } + } + } + } + } Err(err) => format_err!("Github auth failed: {}", err) .status(Status::Unauthorized) .into(), - Ok(github_info) => Outcome::Success(WriteAccess::Github(github_info)), } } @@ -132,7 +192,12 @@ impl<'r> FromRequest<'r> for ReadAccess { pub enum WriteAccess { ApiKey, - Github(GithubInfo), + Github(GithubWriteAccessInfo), +} + +pub enum WritePermission { + Default, + Org, } impl WriteAccess { @@ -140,24 +205,31 @@ impl WriteAccess { &self, package_id: &PackageId, index: &PackageIndex, - ) -> anyhow::Result { + ) -> anyhow::Result> { let scope = package_id.name().scope(); - let has_permission = match self { - WriteAccess::ApiKey => true, + let write_permission = match self { + WriteAccess::ApiKey => Some(WritePermission::Default), WriteAccess::Github(github_info) => { - match index.is_scope_owner(scope, github_info.id())? { - true => true, - // Only grant write access if the username matches the scope AND the scope has no existing owners + match index.is_scope_owner(scope, github_info.user.id())? { + true => Some(WritePermission::Default), + // Only grant write access if the username matches the scope AND the scope has no existing owners or they are a member of the org false => { - github_info.login().to_lowercase() == scope + if github_info.user.login().to_lowercase() == scope && index.get_scope_owners(scope)?.is_empty() + { + Some(WritePermission::Default) + } else if github_info.organizations.contains(&scope.to_string()) { + Some(WritePermission::Org) + } else { + None + } } } } }; - Ok(has_permission) + Ok(write_permission) } } diff --git a/wally-registry-backend/src/main.rs b/wally-registry-backend/src/main.rs index 46733fc1..7d7f98da 100644 --- a/wally-registry-backend/src/main.rs +++ b/wally-registry-backend/src/main.rs @@ -15,6 +15,7 @@ use std::io::{Cursor, Read, Seek}; use std::sync::RwLock; use anyhow::{format_err, Context}; +use auth::WritePermission; use figment::{ providers::{Env, Format, Toml}, Figment, @@ -155,7 +156,9 @@ async fn publish( let manifest = get_manifest(&mut archive).status(Status::BadRequest)?; let package_id = manifest.package_id(); - if !authorization.can_write_package(&package_id, &index)? { + let write_permission = authorization.can_write_package(&package_id, &index)?; + + if write_permission.is_none() { return Err(format_err!( "you do not have permission to write in scope {}", package_id.name().scope() @@ -165,12 +168,17 @@ async fn publish( // If a user can write but isn't in the scope owner file then we should add them! if let WriteAccess::Github(github_info) = authorization { - let user_id = github_info.id(); + let user_id = github_info.user.id(); let scope = package_id.name().scope(); - if !index.is_scope_owner(scope, user_id)? { - index.add_scope_owner(scope, user_id)?; - } + match write_permission.unwrap() { + WritePermission::Default => { + if !index.is_scope_owner(scope, user_id)? { + index.add_scope_owner(scope, user_id)?; + } + } + _ => {} + }; } let package_metadata = index.get_package_metadata(manifest.package_id().name()); From 7f631f92b637841f00512354dd70b5775fd8aa72 Mon Sep 17 00:00:00 2001 From: MobiDev Date: Tue, 13 Jun 2023 12:42:45 +0000 Subject: [PATCH 02/13] handle no user:org permissions --- src/commands/login.rs | 7 +- wally-registry-backend/src/auth.rs | 111 ++++++++++++++++------------- wally-registry-backend/src/main.rs | 2 +- 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 21251854..bf7db5cf 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -91,10 +91,11 @@ fn prompt_github_auth(api: url::Url, github_oauth_id: &str) -> anyhow::Result<() .header("accept", "application/json") .json(&serde_json::json!({ "client_id": github_oauth_id, - "scope": "read:user", + "scope": "read:user read:org", })) - .send()? - .json::()?; + .send()?; + + let device_code_response = device_code_response.json::()?; println!(); println!("Go to {}", device_code_response.verification_uri); diff --git a/wally-registry-backend/src/auth.rs b/wally-registry-backend/src/auth.rs index 59039281..7f5a81ef 100644 --- a/wally-registry-backend/src/auth.rs +++ b/wally-registry-backend/src/auth.rs @@ -27,7 +27,6 @@ pub enum AuthMode { pub struct GithubInfo { login: String, id: u64, - organizations_url: String, } #[derive(Deserialize, Debug)] @@ -37,14 +36,13 @@ pub struct GithubOrgInfoOrganization { #[derive(Deserialize, Debug)] pub struct GithubOrgInfo { - organization: GithubOrgInfoOrganization, - user: GithubInfo, + organization: GithubOrgInfoOrganization } #[derive(Debug)] pub struct GithubWriteAccessInfo { pub user: GithubInfo, - pub organizations: Vec, + pub token: String, } impl GithubInfo { @@ -99,6 +97,38 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome { + return format_err!(err).status(Status::InternalServerError).into(); + } + Ok(response) => response.json::().await, + }; + + match github_info { + Err(err) => format_err!("Github auth failed: {}", err) + .status(Status::Unauthorized) + .into(), + Ok(github_info) => { + return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { + user: github_info, + token: token + })); + } + } +} + +pub async fn get_github_orgs(token: String) -> Result, Error> { + let client = Client::new(); + let org_response = client .get("https://api.github.com/user/memberships/orgs") .header("accept", "application/json") @@ -109,7 +139,7 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome { - return format_err!(err).status(Status::InternalServerError).into(); + return Err(format_err!(err).status(Status::InternalServerError)); } Ok(response) => response.json::>().await, }; @@ -117,49 +147,15 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome { match github_org_info.get(0) { - Some(org) => { - return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { - user: org.user.clone(), - organizations: github_org_info - .iter() - .map(|x| x.organization.login.to_lowercase()) - .collect::>(), - })); - } - None => { - // The user is in no orgs we can see so we cannot get their userinfo from that. - let response = client - .get("https://api.github.com/user") - .header("accept", "application/json") - .header("user-agent", "wally") - .bearer_auth(&token) - .send() - .await; - - let github_info = match response { - Err(err) => { - return format_err!(err).status(Status::InternalServerError).into(); - } - Ok(response) => response.json::().await, - }; - - match github_info { - Err(err) => format_err!("Github auth failed: {}", err) - .status(Status::Unauthorized) - .into(), - Ok(github_info) => { - return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { - user: github_info, - organizations: vec![], - })); - } - } - } + Some(_) => Ok(github_org_info + .iter() + .map(|x| x.organization.login.to_lowercase()) + .collect::>()), + None => Ok(vec![]) } } - Err(err) => format_err!("Github auth failed: {}", err) - .status(Status::Unauthorized) - .into(), + Err(err) => Err(format_err!("Github auth failed: {}", err) + .status(Status::Unauthorized)), } } @@ -201,11 +197,11 @@ pub enum WritePermission { } impl WriteAccess { - pub fn can_write_package( + pub async fn can_write_package( &self, package_id: &PackageId, index: &PackageIndex, - ) -> anyhow::Result> { + ) -> Result, Error> { let scope = package_id.name().scope(); let write_permission = match self { @@ -219,10 +215,23 @@ impl WriteAccess { && index.get_scope_owners(scope)?.is_empty() { Some(WritePermission::Default) - } else if github_info.organizations.contains(&scope.to_string()) { - Some(WritePermission::Org) } else { - None + let orgs = get_github_orgs(github_info.token.clone()).await; + match orgs { + Ok(orgs) => { + if orgs.contains(&scope.to_string()) { + Some(WritePermission::Org) + } else { + None + } + }, + Err(err) => { + return Err(format_err!("Failed to get Github Organisations, do you need to re-login. Error: {:?}", err) + .status(Status::Unauthorized) + .into()) + }, + } + } } } diff --git a/wally-registry-backend/src/main.rs b/wally-registry-backend/src/main.rs index e88f1574..73259fbd 100644 --- a/wally-registry-backend/src/main.rs +++ b/wally-registry-backend/src/main.rs @@ -156,7 +156,7 @@ async fn publish( let manifest = get_manifest(&mut archive).status(Status::BadRequest)?; let package_id = manifest.package_id(); - let write_permission = authorization.can_write_package(&package_id, &index)?; + let write_permission = authorization.can_write_package(&package_id, &index).await?; if write_permission.is_none() { return Err(format_err!( From 22db5e3df4b3fb6a2cff1ce05066229a43a21f8a Mon Sep 17 00:00:00 2001 From: MobiDev Date: Tue, 13 Jun 2023 12:45:47 +0000 Subject: [PATCH 03/13] please the fmt god --- wally-registry-backend/src/auth.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/wally-registry-backend/src/auth.rs b/wally-registry-backend/src/auth.rs index 7f5a81ef..4c643b82 100644 --- a/wally-registry-backend/src/auth.rs +++ b/wally-registry-backend/src/auth.rs @@ -36,7 +36,7 @@ pub struct GithubOrgInfoOrganization { #[derive(Deserialize, Debug)] pub struct GithubOrgInfo { - organization: GithubOrgInfoOrganization + organization: GithubOrgInfoOrganization, } #[derive(Debug)] @@ -120,13 +120,13 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome { return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { user: github_info, - token: token + token: token, })); } } } -pub async fn get_github_orgs(token: String) -> Result, Error> { +pub async fn get_github_orgs(token: String) -> Result, Error> { let client = Client::new(); let org_response = client @@ -145,17 +145,14 @@ pub async fn get_github_orgs(token: String) -> Result, Error> { }; match github_org_info { - Ok(github_org_info) => { - match github_org_info.get(0) { - Some(_) => Ok(github_org_info + Ok(github_org_info) => match github_org_info.get(0) { + Some(_) => Ok(github_org_info .iter() .map(|x| x.organization.login.to_lowercase()) .collect::>()), - None => Ok(vec![]) - } - } - Err(err) => Err(format_err!("Github auth failed: {}", err) - .status(Status::Unauthorized)), + None => Ok(vec![]), + }, + Err(err) => Err(format_err!("Github auth failed: {}", err).status(Status::Unauthorized)), } } @@ -231,7 +228,6 @@ impl WriteAccess { .into()) }, } - } } } From 1c9a104c45a420555b19fd434cca4eb032dca1ce Mon Sep 17 00:00:00 2001 From: MobiDev Date: Tue, 13 Jun 2023 12:48:43 +0000 Subject: [PATCH 04/13] please the fmt godsv --- src/commands/login.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index bf7db5cf..f9767d8a 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -94,7 +94,7 @@ fn prompt_github_auth(api: url::Url, github_oauth_id: &str) -> anyhow::Result<() "scope": "read:user read:org", })) .send()?; - + let device_code_response = device_code_response.json::()?; println!(); From 49fee58cc1e067f9b3916fda4d09dd42ecbf2681 Mon Sep 17 00:00:00 2001 From: magnalite Date: Wed, 14 Jun 2023 04:58:26 +0100 Subject: [PATCH 05/13] Clarify logic and misc upkeep changes --- src/commands/login.rs | 5 +- wally-registry-backend/src/auth.rs | 180 ++++++++++++++--------------- wally-registry-backend/src/main.rs | 23 ++-- 3 files changed, 102 insertions(+), 106 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index f9767d8a..e39c9d97 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -93,9 +93,8 @@ fn prompt_github_auth(api: url::Url, github_oauth_id: &str) -> anyhow::Result<() "client_id": github_oauth_id, "scope": "read:user read:org", })) - .send()?; - - let device_code_response = device_code_response.json::()?; + .send()? + .json::()?; println!(); println!("Go to {}", device_code_response.verification_uri); diff --git a/wally-registry-backend/src/auth.rs b/wally-registry-backend/src/auth.rs index 4c643b82..e5c00bee 100644 --- a/wally-registry-backend/src/auth.rs +++ b/wally-registry-backend/src/auth.rs @@ -1,6 +1,6 @@ use std::fmt; -use anyhow::format_err; +use anyhow::{format_err, Context}; use constant_time_eq::constant_time_eq; use libwally::{package_id::PackageId, package_index::PackageIndex}; use reqwest::Client; @@ -23,35 +23,35 @@ pub enum AuthMode { Unauthenticated, } -#[derive(Deserialize, Clone, Debug)] -pub struct GithubInfo { +#[derive(Deserialize)] +pub struct GithubOrgInfo { login: String, - id: u64, } -#[derive(Deserialize, Debug)] -pub struct GithubOrgInfoOrganization { - login: String, +#[derive(Deserialize)] +pub struct GithubOrgInfoResponse { + organization: GithubOrgInfo, } -#[derive(Deserialize, Debug)] -pub struct GithubOrgInfo { - organization: GithubOrgInfoOrganization, +#[derive(Deserialize)] +pub struct GithubUserInfo { + login: String, + id: u64, } -#[derive(Debug)] -pub struct GithubWriteAccessInfo { - pub user: GithubInfo, - pub token: String, +#[derive(Deserialize)] +pub struct GithubInfo { + user: GithubUserInfo, + orgs: Vec, } impl GithubInfo { pub fn login(&self) -> &str { - &self.login + &self.user.login } pub fn id(&self) -> &u64 { - &self.id + &self.user.id } } @@ -95,65 +95,68 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome format_err!("Github auth failed: {}", err) + .status(Status::Unauthorized) + .into(), + Ok(info) => Outcome::Success(WriteAccess::Github(info)), + } +} + +async fn get_github_user_info(token: &str) -> anyhow::Result { let client = Client::new(); - // The user is in no orgs we can see so we cannot get their userinfo from that. + // Users already logged in may not have given us read:org permission so we + // need to still support a basic read:user check. + // See: https://github.com/UpliftGames/wally/pull/147 + // TODO: Eventually we can transition to only using org level oauth let response = client .get("https://api.github.com/user") .header("accept", "application/json") .header("user-agent", "wally") - .bearer_auth(&token) + .bearer_auth(token) .send() - .await; - - let github_info = match response { - Err(err) => { - return format_err!(err).status(Status::InternalServerError).into(); - } - Ok(response) => response.json::().await, - }; + .await + .context("Github user info request failed!")?; - match github_info { - Err(err) => format_err!("Github auth failed: {}", err) - .status(Status::Unauthorized) - .into(), - Ok(github_info) => { - return Outcome::Success(WriteAccess::Github(GithubWriteAccessInfo { - user: github_info, - token: token, - })); - } - } + response + .json::() + .await + .context("Failed to parse github user info") } -pub async fn get_github_orgs(token: String) -> Result, Error> { +pub async fn get_github_orgs(token: &str) -> Result, Error> { let client = Client::new(); let org_response = client .get("https://api.github.com/user/memberships/orgs") .header("accept", "application/json") .header("user-agent", "wally") - .bearer_auth(&token) + .bearer_auth(token) .send() - .await; + .await + .context("Github org membership request failed")?; - let github_org_info = match org_response { - Err(err) => { - return Err(format_err!(err).status(Status::InternalServerError)); - } - Ok(response) => response.json::>().await, - }; + let github_org_info = org_response + .json::>() + .await + .context("Failed to parse github org membership")?; - match github_org_info { - Ok(github_org_info) => match github_org_info.get(0) { - Some(_) => Ok(github_org_info - .iter() - .map(|x| x.organization.login.to_lowercase()) - .collect::>()), - None => Ok(vec![]), - }, - Err(err) => Err(format_err!("Github auth failed: {}", err).status(Status::Unauthorized)), - } + let orgs: Vec<_> = github_org_info + .iter() + .map(|org_info| org_info.organization.login.to_lowercase()) + .collect(); + + Ok(orgs) } pub enum ReadAccess { @@ -185,11 +188,13 @@ impl<'r> FromRequest<'r> for ReadAccess { pub enum WriteAccess { ApiKey, - Github(GithubWriteAccessInfo), + Github(GithubInfo), } pub enum WritePermission { Default, + Owner, + User, Org, } @@ -198,46 +203,37 @@ impl WriteAccess { &self, package_id: &PackageId, index: &PackageIndex, - ) -> Result, Error> { + ) -> anyhow::Result> { let scope = package_id.name().scope(); - let write_permission = match self { - WriteAccess::ApiKey => Some(WritePermission::Default), - WriteAccess::Github(github_info) => { - match index.is_scope_owner(scope, github_info.user.id())? { - true => Some(WritePermission::Default), - // Only grant write access if the username matches the scope AND the scope has no existing owners or they are a member of the org - false => { - if github_info.user.login().to_lowercase() == scope - && index.get_scope_owners(scope)?.is_empty() - { - Some(WritePermission::Default) - } else { - let orgs = get_github_orgs(github_info.token.clone()).await; - match orgs { - Ok(orgs) => { - if orgs.contains(&scope.to_string()) { - Some(WritePermission::Org) - } else { - None - } - }, - Err(err) => { - return Err(format_err!("Failed to get Github Organisations, do you need to re-login. Error: {:?}", err) - .status(Status::Unauthorized) - .into()) - }, - } - } - } - } - } - }; - - Ok(write_permission) + match self { + WriteAccess::ApiKey => Ok(Some(WritePermission::Default)), + WriteAccess::Github(info) => github_write_permission_for_scope(info, scope, index), + } } } +fn github_write_permission_for_scope( + info: &GithubInfo, + scope: &str, + index: &PackageIndex, +) -> anyhow::Result> { + Ok(match index.is_scope_owner(scope, info.id())? { + true => Some(WritePermission::Owner), + false => { + // Only grant write access if the username matches the scope AND the scope has no existing owners + if info.login().to_lowercase() == scope && index.get_scope_owners(scope)?.is_empty() { + Some(WritePermission::User) + // ... or if they are in the organization! + } else if info.orgs.contains(&scope.to_string()) { + Some(WritePermission::Org) + } else { + None + } + } + }) +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for WriteAccess { type Error = Error; diff --git a/wally-registry-backend/src/main.rs b/wally-registry-backend/src/main.rs index 73259fbd..4d507831 100644 --- a/wally-registry-backend/src/main.rs +++ b/wally-registry-backend/src/main.rs @@ -156,7 +156,7 @@ async fn publish( let manifest = get_manifest(&mut archive).status(Status::BadRequest)?; let package_id = manifest.package_id(); - let write_permission = authorization.can_write_package(&package_id, &index).await?; + let write_permission = authorization.can_write_package(&package_id, index).await?; if write_permission.is_none() { return Err(format_err!( @@ -167,18 +167,19 @@ async fn publish( } // If a user can write but isn't in the scope owner file then we should add them! - if let WriteAccess::Github(github_info) = authorization { - let user_id = github_info.user.id(); + if let WriteAccess::Github(github) = authorization { + let user_id = github.id(); let scope = package_id.name().scope(); - match write_permission.unwrap() { - WritePermission::Default => { - if !index.is_scope_owner(scope, user_id)? { - index.add_scope_owner(scope, user_id)?; - } + // However we should only do this if they are the user matching this scope! + // If they have permission due to being a member of an org we want to leave + // the permission up to the org membership so if they are removed from the + // org they automatically lose write permission. + if let WritePermission::User = write_permission.unwrap() { + if !index.is_scope_owner(scope, user_id)? { + index.add_scope_owner(scope, user_id)?; } - _ => {} - }; + } } let package_metadata = index.get_package_metadata(manifest.package_id().name()); @@ -203,7 +204,7 @@ async fn publish( if let Ok(mut search_backend) = search_backend.try_write() { // TODO: Recrawling the whole index for each publish is very wasteful! // Eventually this will get too expensive and we should only add the new package. - search_backend.crawl_packages(&index)?; + search_backend.crawl_packages(index)?; } Ok(Json(json!({ From 3d3d0472d7c45c53260fbef40d79f5c2c3f7f23d Mon Sep 17 00:00:00 2001 From: magnalite Date: Tue, 13 Jun 2023 01:26:35 +0100 Subject: [PATCH 06/13] Nightly build workflow (#148) * Nightly workflow --- .github/workflows/build.yml | 113 ++++++++++++++++++++++++++ .github/workflows/nightly.yml | 40 +++++++++ .github/workflows/release.yml | 134 +++---------------------------- .github/workflows/test-merge.yml | 1 + 4 files changed, 163 insertions(+), 125 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/nightly.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..cce26efa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,113 @@ +on: + workflow_call: + inputs: + upload_url: + required: true + type: string + + release_ref: + required: true + type: string + +jobs: + windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - uses: Swatinem/rust-cache@v2 + + - name: Build release binary + run: cargo build --verbose --locked --release + + - name: Create Release Archive + shell: bash + run: | + mkdir staging + cp "target/release/wally.exe" staging/ + cd staging + 7z a ../release.zip * + + - name: Upload Archive to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ inputs.upload_url }} + asset_path: release.zip + asset_name: wally-${{ inputs.release_ref }}-win64.zip + asset_content_type: application/octet-stream + + macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - name: Install Rust + run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + + - uses: Swatinem/rust-cache@v2 + + - name: Build release binary + run: | + source $HOME/.cargo/env + cargo build --verbose --locked --release + env: + OPENSSL_STATIC: 1 + + - name: Create Release Archive + shell: bash + run: | + mkdir staging + cp "target/release/wally" staging/ + cd staging + zip ../release.zip * + + - name: Upload Archive to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ inputs.upload_url }} + asset_path: release.zip + asset_name: wally-${{ inputs.release_ref }}-macos.zip + asset_content_type: application/octet-stream + + linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --locked --verbose --release + env: + OPENSSL_STATIC: 1 + + - name: Create Release Archive + shell: bash + run: | + mkdir staging + cp "target/release/wally" staging/ + cd staging + zip ../release.zip * + + - name: Upload Archive to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ inputs.upload_url }} + asset_path: release.zip + asset_name: wally-${{ inputs.release_ref }}-linux.zip + asset_content_type: application/octet-stream \ No newline at end of file diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..5722802c --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,40 @@ +name: Nightly + +on: + push: + branches: + - main + pull_request: + types: [labeled] + +jobs: + fetch_tag: + if: | + github.event_name == 'push' || + contains(github.event.pull_request.labels.*.name, 'built-as-nightly') + runs-on: ubuntu-latest + name: Build nightly release + outputs: + previous_tag: ${{ steps.previoustag.outputs.tag }} + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - name: 'Get Previous tag' + id: previoustag + uses: actions-ecosystem/action-get-latest-tag@v1 + with: + semver_only: true + + - name: Output tag + shell: bash + run: echo "tag=${{ steps.previoustag.outputs.tag }}" >> "$GITHUB_OUTPUT" + + build: + uses: ./.github/workflows/build.yml + needs: fetch_tag + with: + upload_url: https://uploads.github.com/repos/UpliftGames/wally/releases/108261310/assets{?name,label} + release_ref: ${{ needs.fetch_tag.outputs.previous_tag }}-nightly \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a3fe467..bcd78056 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,17 +2,17 @@ name: Release on: push: - tags: ["*"] + tags: ["v*"] jobs: create-release: + if: endsWith(github.ref, 'main') name: Create Release runs-on: ubuntu-latest outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ steps.create-release.outputs.upload_url }} steps: - name: Create Release - id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -22,125 +22,9 @@ jobs: draft: true prerelease: false - windows: - needs: ["create-release"] - runs-on: windows-latest - - steps: - - uses: actions/checkout@v1 - with: - submodules: true - - - uses: Swatinem/rust-cache@v2 - - - name: Build release binary - run: cargo build --verbose --locked --release - - - name: Create Release Archive - shell: bash - run: | - mkdir staging - cp "target/release/wally.exe" staging/ - cd staging - 7z a ../release.zip * - - - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: release.zip - asset_name: wally-${{github.ref_name}}-win64.zip - asset_content_type: application/octet-stream - - - name: Upload artifacts - uses: actions/upload-artifact@v1 - with: - name: wally-${{ github.ref_name }}-win64.zip - path: release.zip - - macos: - needs: ["create-release"] - runs-on: macos-latest - - steps: - - uses: actions/checkout@v1 - with: - submodules: true - - - name: Install Rust - run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - - - uses: Swatinem/rust-cache@v2 - - - name: Build release binary - run: | - source $HOME/.cargo/env - cargo build --verbose --locked --release - env: - OPENSSL_STATIC: 1 - - - name: Create Release Archive - shell: bash - run: | - mkdir staging - cp "target/release/wally" staging/ - cd staging - zip ../release.zip * - - - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: release.zip - asset_name: wally-${{ github.ref_name }}-macos.zip - asset_content_type: application/octet-stream - - - name: Upload artifacts - uses: actions/upload-artifact@v1 - with: - name: wally-${{ github.ref_name }}-macos.zip - path: release.zip - - linux: - needs: ["create-release"] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - with: - submodules: true - - - uses: Swatinem/rust-cache@v2 - - - name: Build - run: cargo build --locked --verbose --release - env: - OPENSSL_STATIC: 1 - - - name: Create Release Archive - shell: bash - run: | - mkdir staging - cp "target/release/wally" staging/ - cd staging - zip ../release.zip * - - - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: release.zip - asset_name: wally-${{ github.ref_name }}-linux.zip - asset_content_type: application/octet-stream - - - name: Upload artifacts - uses: actions/upload-artifact@v1 - with: - name: wally-${{ github.ref_name }}-linux.zip - path: release.zip + build-release: + uses: ./.github/workflows/build.yml + needs: create-release + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + release_ref: ${{ github.ref_name }} \ No newline at end of file diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index 82381ce4..e62214e4 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -4,6 +4,7 @@ on: types: [labeled] jobs: merge-branch: + if: contains(github.event.pull_request.labels.*.name, 'merged to test') runs-on: ubuntu-latest steps: - uses: actions/checkout@master From 981b8b8cbc3c622e390cca9c68589b35e244f7b9 Mon Sep 17 00:00:00 2001 From: utrain <63367489+u-train@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:41:37 -0400 Subject: [PATCH 07/13] Update subcommand (#140) * Refactored TempProject into its own module and started on tests * Created test projects with a diamond-graph This will allow us to test the update tool in all expected situations - One dependency added/removed - One dependency updated - Transitive dependencies changed It's also a nice addition for testing any other commands as well. * Added to primary-registry Metadata and zipped up * Built the update subcommand. * Lockfiles need to be written! * Fixed misspelling of dependency_changes * Reverted test_registry arg back to invisible * Changed target_packages to package_specs Clarity? * Downgrading was added as a concept * Added lockfile method to view packages as packageIds * Installs the new packages now, forgor. * No longer throws if missing lockfile It will generate a new one instead based on the manifest. * Clarity on the filtering process for try_to_use * Added needed imports -- they got stashed by accident * Made the clippy happy and fixed grammar mistakes * Delete root from registry They're not packages to be consumed. * Add new tests, almost done with them all so far. * Added the rest of snapshots missing * Testing now works, yay! * Deleted testing code by mistake and forgot snap They say to run your tests before push.. This is why. * Appleasing the formatter Import in the wrong order... Extra newline at the end... * Made it look... *pretty* and cleaned messes * another blood sarciface for rust fmt * Doing final cleanups per comments upstream * The gods demand it, another formatter sacrifice Co-authored-by: magnalite * The coolness must be toned down. * A little silly mistake indeed Seemingly forgot to filter out packages to update for try_to_use. instead it kept them and got rid of everything else. --------- Co-authored-by: magnalite --- src/commands/mod.rs | 4 +- src/commands/update.rs | 303 +++++++++++++++++- src/lockfile.rs | 9 + .../0.1.0/default.project.json | 9 + .../direct-dependency-a/0.1.0/src/init.lua | 3 + .../direct-dependency-a/0.1.0/wally.toml | 9 + .../0.1.1/default.project.json | 9 + .../direct-dependency-a/0.1.1/src/init.lua | 3 + .../direct-dependency-a/0.1.1/wally.toml | 9 + .../0.1.0/default.project.json | 9 + .../direct-dependency-b/0.1.0/src/init.lua | 3 + .../direct-dependency-b/0.1.0/wally.toml | 9 + .../0.1.0/default.project.json | 6 + .../indirect-dependency-a/0.1.0/src/init.lua | 3 + .../indirect-dependency-a/0.1.0/wally.toml | 6 + .../0.1.1/default.project.json | 6 + .../indirect-dependency-a/0.1.1/src/init.lua | 4 + .../indirect-dependency-a/0.1.1/wally.toml | 6 + .../0.2.0/default.project.json | 6 + .../indirect-dependency-a/0.2.0/src/init.lua | 3 + .../indirect-dependency-a/0.2.0/wally.toml | 6 + .../0.2.1/default.project.json | 6 + .../indirect-dependency-a/0.2.1/src/init.lua | 4 + .../indirect-dependency-a/0.2.1/wally.toml | 6 + .../root/dated/default.project.json | 9 + .../diamond-graph/root/dated/src/init.lua | 4 + .../diamond-graph/root/dated/wally.lock | 28 ++ .../diamond-graph/root/dated/wally.toml | 10 + .../root/fresh/default.project.json | 9 + .../diamond-graph/root/fresh/src/init.lua | 4 + .../diamond-graph/root/fresh/wally.toml | 10 + .../direct-dependency-a/0.1.0.zip | Bin 0 -> 938 bytes .../direct-dependency-a/0.1.1.zip | Bin 0 -> 938 bytes .../direct-dependency-b/0.1.0.zip | Bin 0 -> 909 bytes .../indirect-dependency-a/0.1.0.zip | Bin 0 -> 842 bytes .../indirect-dependency-a/0.1.1.zip | Bin 0 -> 850 bytes .../indirect-dependency-a/0.2.0.zip | Bin 0 -> 841 bytes .../indirect-dependency-a/0.2.1.zip | Bin 0 -> 871 bytes .../index/diamond-graph/direct-dependency-a | 2 + .../index/diamond-graph/direct-dependency-b | 1 + .../index/diamond-graph/indirect-dependency-a | 4 + tests/integration/install.rs | 58 +--- tests/integration/main.rs | 2 + ...tion__update__update_all_dependencies.snap | 33 ++ ...gration__update__update_list_of_specs.snap | 33 ++ ...tion__update__update_named_dependency.snap | 33 ++ ...n__update__update_required_dependency.snap | 33 ++ tests/integration/temp_project.rs | 55 ++++ tests/integration/update.rs | 152 +++++++++ 49 files changed, 860 insertions(+), 61 deletions(-) create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.0/default.project.json create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.0/src/init.lua create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.0/wally.toml create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.1/default.project.json create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.1/src/init.lua create mode 100644 test-projects/diamond-graph/direct-dependency-a/0.1.1/wally.toml create mode 100644 test-projects/diamond-graph/direct-dependency-b/0.1.0/default.project.json create mode 100644 test-projects/diamond-graph/direct-dependency-b/0.1.0/src/init.lua create mode 100644 test-projects/diamond-graph/direct-dependency-b/0.1.0/wally.toml create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.0/default.project.json create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.0/src/init.lua create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.0/wally.toml create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.1/default.project.json create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.1/src/init.lua create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.1.1/wally.toml create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.0/default.project.json create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.0/src/init.lua create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.0/wally.toml create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.1/default.project.json create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.1/src/init.lua create mode 100644 test-projects/diamond-graph/indirect-dependency-a/0.2.1/wally.toml create mode 100644 test-projects/diamond-graph/root/dated/default.project.json create mode 100644 test-projects/diamond-graph/root/dated/src/init.lua create mode 100644 test-projects/diamond-graph/root/dated/wally.lock create mode 100644 test-projects/diamond-graph/root/dated/wally.toml create mode 100644 test-projects/diamond-graph/root/fresh/default.project.json create mode 100644 test-projects/diamond-graph/root/fresh/src/init.lua create mode 100644 test-projects/diamond-graph/root/fresh/wally.toml create mode 100644 test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.0.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.1.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/direct-dependency-b/0.1.0.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.1.0.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.1.1.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.2.0.zip create mode 100644 test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.2.1.zip create mode 100644 test-registries/primary-registry/index/diamond-graph/direct-dependency-a create mode 100644 test-registries/primary-registry/index/diamond-graph/direct-dependency-b create mode 100644 test-registries/primary-registry/index/diamond-graph/indirect-dependency-a create mode 100644 tests/integration/snapshots/integration__update__update_all_dependencies.snap create mode 100644 tests/integration/snapshots/integration__update__update_list_of_specs.snap create mode 100644 tests/integration/snapshots/integration__update__update_named_dependency.snap create mode 100644 tests/integration/snapshots/integration__update__update_required_dependency.snap create mode 100644 tests/integration/temp_project.rs create mode 100644 tests/integration/update.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9315c808..d16ae071 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -16,7 +16,7 @@ pub use manifest_to_json::ManifestToJsonSubcommand; pub use package::PackageSubcommand; pub use publish::PublishSubcommand; pub use search::SearchSubcommand; -pub use update::UpdateSubcommand; +pub use update::{PackageSpec, UpdateSubcommand}; use structopt::StructOpt; @@ -37,7 +37,7 @@ impl Args { Subcommand::Init(subcommand) => subcommand.run(), Subcommand::Login(subcommand) => subcommand.run(), Subcommand::Logout(subcommand) => subcommand.run(), - Subcommand::Update(subcommand) => subcommand.run(), + Subcommand::Update(subcommand) => subcommand.run(self.global), Subcommand::Search(subcommand) => subcommand.run(), Subcommand::Package(subcommand) => subcommand.run(), Subcommand::Install(subcommand) => subcommand.run(self.global), diff --git a/src/commands/update.rs b/src/commands/update.rs index 4d65d24e..e23e0078 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,11 +1,308 @@ +use std::collections::BTreeSet; +use std::convert::TryInto; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +use crate::installation::InstallationContext; +use crate::lockfile::Lockfile; +use crate::manifest::Manifest; +use crate::package_id::PackageId; +use crate::package_name::PackageName; +use crate::package_req::PackageReq; +use crate::package_source::{PackageSource, PackageSourceMap, Registry, TestRegistry}; +use crate::{resolution, GlobalOptions}; +use crossterm::style::{Attribute, Color, SetAttribute, SetForegroundColor}; +use indicatif::{ProgressBar, ProgressStyle}; use structopt::StructOpt; /// Update all of the dependencies of this project. #[derive(Debug, StructOpt)] -pub struct UpdateSubcommand {} +pub struct UpdateSubcommand { + /// Path to the project to publish. + #[structopt(long = "project-path", default_value = ".")] + pub project_path: PathBuf, + + /// An optional list of dependencies to update. + /// They must be valid package name with an optional version requirement. + pub package_specs: Vec, +} impl UpdateSubcommand { - pub fn run(self) -> anyhow::Result<()> { - todo!("The 'update' subcommand") + pub fn run(self, global: GlobalOptions) -> anyhow::Result<()> { + let manifest = Manifest::load(&self.project_path)?; + + let lockfile = match Lockfile::load(&self.project_path)? { + Some(lockfile) => lockfile, + None => Lockfile::from_manifest(&manifest), + }; + + let default_registry: Box = if global.test_registry { + Box::new(PackageSource::TestRegistry(TestRegistry::new( + &manifest.package.registry, + ))) + } else { + Box::new(PackageSource::Registry(Registry::from_registry_spec( + &manifest.package.registry, + )?)) + }; + + let mut package_sources = PackageSourceMap::new(default_registry); + package_sources.add_fallbacks()?; + + // If the user didn't specify any targets, then update all of the packages. + // Otherwise, find the target packages to update. + let try_to_use = if self.package_specs.is_empty() { + println!( + "{} Selected {} all dependencies to try update", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + ); + + BTreeSet::new() + } else { + let try_to_use: BTreeSet = lockfile + .as_ids() + // We update the target packages by removing the package from the list of packages to try to keep. + .filter(|package_id| !self.given_package_id_satisifies_targets(package_id)) + .collect(); + + println!( + "{} Selected {}{} dependencies to try update", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset), + lockfile.packages.len() - try_to_use.len(), + ); + + try_to_use + }; + + let progress = ProgressBar::new(0) + .with_style( + ProgressStyle::with_template("{spinner:.cyan}{wide_msg}")?.tick_chars("⠁⠈⠐⠠⠄⠂ "), + ) + .with_message(format!( + "{} Resolving {}new dependencies...", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + let resolved_graph = resolution::resolve(&manifest, &try_to_use, &package_sources)?; + + progress.println(format!( + "{} Resolved {}{} total dependencies", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset), + resolved_graph.activated.len() - 1 + )); + + progress.enable_steady_tick(Duration::from_millis(100)); + progress.suspend(|| { + let dependency_changes = + generate_depedency_changes(&lockfile.as_ids().collect(), &resolved_graph.activated); + render_update_difference(&dependency_changes); + }); + + Lockfile::from_resolve(&resolved_graph).save(&self.project_path)?; + + progress.println(format!( + "{} Updated {}lockfile", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + let root_package_id = manifest.package_id(); + let installation_context = InstallationContext::new( + &self.project_path, + manifest.place.shared_packages, + manifest.place.server_packages, + ); + + progress.set_message(format!( + "{} Cleaning {}package destination...", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + installation_context.clean()?; + + progress.println(format!( + "{} Cleaned {}package destination", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + progress.finish_with_message(format!( + "{}{} Starting installation {}", + SetAttribute(Attribute::Bold), + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + )); + + installation_context.install(package_sources, root_package_id, resolved_graph)?; + + Ok(()) + } + + fn given_package_id_satisifies_targets(&self, package_id: &PackageId) -> bool { + self.package_specs + .iter() + .any(|target_package| match target_package { + PackageSpec::Named(named_target) => package_id.name() == named_target, + PackageSpec::Required(required_target) => { + required_target.matches(package_id.name(), package_id.version()) + } + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum PackageSpec { + Named(PackageName), + Required(PackageReq), +} + +impl FromStr for PackageSpec { + type Err = anyhow::Error; + + fn from_str(value: &str) -> anyhow::Result { + if let Ok(package_req) = value.parse() { + Ok(PackageSpec::Required(package_req)) + } else if let Ok(package_name) = value.parse() { + Ok(PackageSpec::Named(package_name)) + } else { + anyhow::bail!( + "Was unable to parse {} into a package requirement or a package name!", + value + ) + } + } +} + +enum DependencyChange { + Added(PackageId), + Removed(PackageId), + Updated { from: PackageId, to: PackageId }, + Downgrade { from: PackageId, to: PackageId }, +} + +fn generate_depedency_changes( + old_dependencies: &BTreeSet, + new_dependencies: &BTreeSet, +) -> Vec { + let added_packages = new_dependencies.difference(old_dependencies); + let removed_packages = old_dependencies.difference(new_dependencies); + let changed_dependencies: BTreeSet<&PackageName> = added_packages + .clone() + .chain(removed_packages.clone()) + .map(|package| package.name()) + .collect(); + + let mut dependency = Vec::new(); + + for dependency_name in changed_dependencies { + let matching_packages_removed = removed_packages + .clone() + .filter(|x| *x.name() == *dependency_name); + let matching_packages_added = added_packages + .clone() + .filter(|x| *x.name() == *dependency_name); + + match ( + matching_packages_added.clone().count(), + matching_packages_removed.clone().count(), + ) { + (1, 1) => { + // We know for certain that there is only one item in the iterator. + let package_added = matching_packages_added.last().unwrap(); + let package_removed = matching_packages_removed.last().unwrap(); + + if package_added.gt(package_removed) { + dependency.push(DependencyChange::Updated { + from: package_removed.clone(), + to: package_added.clone(), + }); + } else { + dependency.push(DependencyChange::Downgrade { + from: package_added.clone(), + to: package_removed.clone(), + }); + } + } + (0, 1) => { + // We know for certain that there is only one item in the iterator. + let package_removed = matching_packages_removed.last().unwrap(); + dependency.push(DependencyChange::Removed(package_removed.clone())); + } + (1, 0) => { + // We know for certain that there is only one item in the iterator. + let package_added = matching_packages_added.last().unwrap(); + dependency.push(DependencyChange::Added(package_added.clone())); + } + (0, 0) => panic!("Impossible for the package name {} to not be removed or added if found in earlier.", dependency_name), + (_, _) => { + dependency.extend(matching_packages_added.map(|package| DependencyChange::Added(package.clone()))); + dependency.extend(matching_packages_removed.map(|package| DependencyChange::Removed(package.clone()))); + } + } + } + + dependency +} + +fn render_update_difference(dependency_changes: &[DependencyChange]) { + if dependency_changes.is_empty() { + return println!( + "{} No Dependency changes{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + ); + } + + println!( + "{} Dependency changes{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset) + ); + + for dependency_change in dependency_changes { + match dependency_change { + DependencyChange::Added(package_id) => { + println!( + "{} Added {}{} v{}", + SetForegroundColor(Color::DarkGreen), + SetForegroundColor(Color::Reset), + package_id.name(), + package_id.version() + ); + } + DependencyChange::Removed(package_id) => { + println!( + "{} Removed {}{} v{}", + SetForegroundColor(Color::DarkRed), + SetForegroundColor(Color::Reset), + package_id.name(), + package_id.version() + ); + } + DependencyChange::Updated { from, to } => { + println!( + "{} Updated {}{} from v{} to v{}", + SetForegroundColor(Color::DarkCyan), + SetForegroundColor(Color::Reset), + from.name(), + from.version(), + to.version() + ); + } + DependencyChange::Downgrade { from, to } => println!( + "{} Downgraded {}{} from v{} to v{}", + SetForegroundColor(Color::DarkYellow), + SetForegroundColor(Color::Reset), + from.name(), + from.version(), + to.version() + ), + } } } diff --git a/src/lockfile.rs b/src/lockfile.rs index 683b5d60..85097367 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -102,6 +102,15 @@ impl Lockfile { Ok(()) } + + pub fn as_ids(&self) -> impl Iterator + '_ { + self.packages.iter().map(|lock_package| match lock_package { + LockPackage::Registry(lock_package) => { + PackageId::new(lock_package.name.clone(), lock_package.version.clone()) + } + LockPackage::Git(_) => todo!(), + }) + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.0/default.project.json b/test-projects/diamond-graph/direct-dependency-a/0.1.0/default.project.json new file mode 100644 index 00000000..3852cac0 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.0/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "direct-dependency-a", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.0/src/init.lua b/test-projects/diamond-graph/direct-dependency-a/0.1.0/src/init.lua new file mode 100644 index 00000000..f65f24ab --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.0/src/init.lua @@ -0,0 +1,3 @@ +local Indirect = require(script.Parent.Indirect) + +return if Indirect == 420 then "Bingo!" else "Naw..." \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.0/wally.toml b/test-projects/diamond-graph/direct-dependency-a/0.1.0/wally.toml new file mode 100644 index 00000000..218e27a1 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.0/wally.toml @@ -0,0 +1,9 @@ +[package] +name = "diamond-graph/direct-dependency-a" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +Indirect = "diamond-graph/indirect-dependency-a@0.1.0" \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.1/default.project.json b/test-projects/diamond-graph/direct-dependency-a/0.1.1/default.project.json new file mode 100644 index 00000000..3852cac0 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.1/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "direct-dependency-a", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.1/src/init.lua b/test-projects/diamond-graph/direct-dependency-a/0.1.1/src/init.lua new file mode 100644 index 00000000..f65f24ab --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.1/src/init.lua @@ -0,0 +1,3 @@ +local Indirect = require(script.Parent.Indirect) + +return if Indirect == 420 then "Bingo!" else "Naw..." \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-a/0.1.1/wally.toml b/test-projects/diamond-graph/direct-dependency-a/0.1.1/wally.toml new file mode 100644 index 00000000..172081f8 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-a/0.1.1/wally.toml @@ -0,0 +1,9 @@ +[package] +name = "diamond-graph/direct-dependency-a" +version = "0.1.1" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +Indirect = "diamond-graph/indirect-dependency-a@0.1.1" \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-b/0.1.0/default.project.json b/test-projects/diamond-graph/direct-dependency-b/0.1.0/default.project.json new file mode 100644 index 00000000..89818808 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-b/0.1.0/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "direct-dependency-b", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-b/0.1.0/src/init.lua b/test-projects/diamond-graph/direct-dependency-b/0.1.0/src/init.lua new file mode 100644 index 00000000..e23aac78 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-b/0.1.0/src/init.lua @@ -0,0 +1,3 @@ +local Indirect = require(script.Parent.Indirect) + +return Indirect == 420 \ No newline at end of file diff --git a/test-projects/diamond-graph/direct-dependency-b/0.1.0/wally.toml b/test-projects/diamond-graph/direct-dependency-b/0.1.0/wally.toml new file mode 100644 index 00000000..1068f477 --- /dev/null +++ b/test-projects/diamond-graph/direct-dependency-b/0.1.0/wally.toml @@ -0,0 +1,9 @@ +[package] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +Indirect = "diamond-graph/indirect-dependency-a@0.2.0" \ No newline at end of file diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.0/default.project.json b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/default.project.json new file mode 100644 index 00000000..d0b3b33a --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "indirect-dependency-a", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.0/src/init.lua b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/src/init.lua new file mode 100644 index 00000000..7c26b3b5 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/src/init.lua @@ -0,0 +1,3 @@ +return function() + return 420 +end diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.0/wally.toml b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/wally.toml new file mode 100644 index 00000000..a20f90a4 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.0/wally.toml @@ -0,0 +1,6 @@ +[package] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.1/default.project.json b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/default.project.json new file mode 100644 index 00000000..d0b3b33a --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "indirect-dependency-a", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.1/src/init.lua b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/src/init.lua new file mode 100644 index 00000000..da76bcb7 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/src/init.lua @@ -0,0 +1,4 @@ +-- Zesty +return function() + return 420 +end diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.1.1/wally.toml b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/wally.toml new file mode 100644 index 00000000..eec95ca6 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.1.1/wally.toml @@ -0,0 +1,6 @@ +[package] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.0/default.project.json b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/default.project.json new file mode 100644 index 00000000..d0b3b33a --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "indirect-dependency-a", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.0/src/init.lua b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/src/init.lua new file mode 100644 index 00000000..be2dca9e --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/src/init.lua @@ -0,0 +1,3 @@ +return function() + return 42 +end diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.0/wally.toml b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/wally.toml new file mode 100644 index 00000000..409b9172 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.0/wally.toml @@ -0,0 +1,6 @@ +[package] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.1/default.project.json b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/default.project.json new file mode 100644 index 00000000..d0b3b33a --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "indirect-dependency-a", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.1/src/init.lua b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/src/init.lua new file mode 100644 index 00000000..7765d5f9 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/src/init.lua @@ -0,0 +1,4 @@ +-- Deviously breaking everything +return function() + return 69 +end diff --git a/test-projects/diamond-graph/indirect-dependency-a/0.2.1/wally.toml b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/wally.toml new file mode 100644 index 00000000..816156e1 --- /dev/null +++ b/test-projects/diamond-graph/indirect-dependency-a/0.2.1/wally.toml @@ -0,0 +1,6 @@ +[package] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.1" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" diff --git a/test-projects/diamond-graph/root/dated/default.project.json b/test-projects/diamond-graph/root/dated/default.project.json new file mode 100644 index 00000000..a66ff578 --- /dev/null +++ b/test-projects/diamond-graph/root/dated/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "root", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/root/dated/src/init.lua b/test-projects/diamond-graph/root/dated/src/init.lua new file mode 100644 index 00000000..fc684338 --- /dev/null +++ b/test-projects/diamond-graph/root/dated/src/init.lua @@ -0,0 +1,4 @@ +local A = require(script.Parent.A) +local B = require(script.Parent.B) + +return `{A} {B}` \ No newline at end of file diff --git a/test-projects/diamond-graph/root/dated/wally.lock b/test-projects/diamond-graph/root/dated/wally.lock new file mode 100644 index 00000000..3e07e02d --- /dev/null +++ b/test-projects/diamond-graph/root/dated/wally.lock @@ -0,0 +1,28 @@ +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.0"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.0"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.0" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.0" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.0"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] diff --git a/test-projects/diamond-graph/root/dated/wally.toml b/test-projects/diamond-graph/root/dated/wally.toml new file mode 100644 index 00000000..9b77c47d --- /dev/null +++ b/test-projects/diamond-graph/root/dated/wally.toml @@ -0,0 +1,10 @@ +[package] +name = "diamond-graph/root" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +A = "diamond-graph/direct-dependency-a@0.1.0" +B = "diamond-graph/direct-dependency-b@0.1.0" \ No newline at end of file diff --git a/test-projects/diamond-graph/root/fresh/default.project.json b/test-projects/diamond-graph/root/fresh/default.project.json new file mode 100644 index 00000000..a66ff578 --- /dev/null +++ b/test-projects/diamond-graph/root/fresh/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "root", + "tree": { + "Packages": { + "$path": "Packages" + }, + "$path": "src" + } +} \ No newline at end of file diff --git a/test-projects/diamond-graph/root/fresh/src/init.lua b/test-projects/diamond-graph/root/fresh/src/init.lua new file mode 100644 index 00000000..fc684338 --- /dev/null +++ b/test-projects/diamond-graph/root/fresh/src/init.lua @@ -0,0 +1,4 @@ +local A = require(script.Parent.A) +local B = require(script.Parent.B) + +return `{A} {B}` \ No newline at end of file diff --git a/test-projects/diamond-graph/root/fresh/wally.toml b/test-projects/diamond-graph/root/fresh/wally.toml new file mode 100644 index 00000000..9b77c47d --- /dev/null +++ b/test-projects/diamond-graph/root/fresh/wally.toml @@ -0,0 +1,10 @@ +[package] +name = "diamond-graph/root" +version = "0.1.0" +license = "MIT" +realm = "server" +registry = "test-registries/primary-registry" + +[server-dependencies] +A = "diamond-graph/direct-dependency-a@0.1.0" +B = "diamond-graph/direct-dependency-b@0.1.0" \ No newline at end of file diff --git a/test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.0.zip b/test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..a22c6ec7f94f47bdea48bbf6f8bc8d83d898193a GIT binary patch literal 938 zcmWIWW@h1H00FJ}C&IuCD8a%Y!%$q5tREV}$-w*|t0@J9ODnh;7+GF0GcbUO0HAIW zpn495>kChW8MmoTiv;r0ftUxXJ2Nk{L@%c_5p2rWJglaiKI!jwCh)@9@AsWn4FQe#TWl4qJ4 zmZ{H{KG)XLT=Gn0xt7LH)oHI^zSLx7KsfI)(0M$p_j!YWPRj#g5uo!@QqvMkb4v6I zit@8klS}lniu3cp&g9I;>de*89)+CpKI0p@DJZ}T?9_|Cp1K}qy*2mxHGZm7n~qH4nUJ@GCtycdJ9p%&9U77vQKv(>k=*xa z?TN7PTT=ylfo^*Y#9To4l_%!pRO*%F=jMQ2HKPTqt8)7c`3@QIxR%dfbZXu<8HUcS z%+k?U4|=Uhy0~MaaOkPGnHn98ZuWci_Zu#%e&8d_!IpKd_hV0@Xny+9iuX3!J9BJZ zh5R3NxiU*{Fp#I0Ot%CR{#J2 literal 0 HcmV?d00001 diff --git a/test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.1.zip b/test-registries/primary-registry/contents/diamond-graph/direct-dependency-a/0.1.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..6aa16213b9f95b63448c7abaf2822942a677a5e6 GIT binary patch literal 938 zcmWIWW@h1H00FJ}C&IuCD8a%Y!%$q5tREV}$-sO+t0@J9ODnh;7+GF0GcbUO0HAIW zpn495s|!zr8MmoTiv;r0ftUxXJ2Nk{L@%c_5p2rGJglaiKI!jwCh)@9@AsWn4FQe#TWl4qJ4 zmZ{H{KG)XLT=Gn0xt7LH)oHI^zSLx7KsfIq(0M$p_j!YWPRj#g5uo!@QqvMkb4v6I zit@8klS}lniu3cp&ScBS>de*89)+CpKI0p@DJZ}T?9_|Cp1K}qy*2mxHGZm7n~qH4nUJ@GCtycdJ9p%&9U77vQKv(>k=*xa z?TN5?lKbp?fo^*Y#9To4l_%!pRO*%F=jMQ2HKPTqt8)7c`3@QIxR%dfbZXu-Ifl-y z%+k?U4|=Uhy0~MaaOkPGnHn98ZuWci_Zu#%e&8d_!IpKd_hV0@Xny+9iuX3!J9BJZ zh5R3NxiU*%8&ME~9I@89_voS67|_Q_A`Uy6>{G6r}vGRZOHN>36%kAZ*y!&^rX4M|acDy?V8+e#O?KkNvJ#Mv)7jdE|Z-*x`MpDx<5_Wazkr|gd_t4!u~9v{l;sz zYX$-B&I4i*p#3SSX^EvdC3*!#`B|ySC3;!K`FUV>xMO$6>SvEaPI;g44c!zJ;01Qe zMPE-{kF(yMXT8^5@eR7fcxt-H(xxY?cUo9ZWfGdaS@?2r_re-K)u~NKCh<(j+rbmC zBdnb}a@7tENsXw}q1;IBd$jgM*e1ONw*5f2JqBVfp!>=bb8;&6O7e4az^n?uXmklo15sK^!C89Hmey&w8VO)Ed_)A&1-9tu=-Wvzc1S5`;ksJ0iG*! zq#jEiR=Vfc_3>h5SkcX@E|qylM61m|DEaR3*!uh1g>P(WWhyT>@8=0;_OfWNQoVF_ zUBaU&TSKO4rv7I2S=oJxaqFJ|Lx%0S-~aEfQs&w8^o?=kw}ln&ZaF{2XM44FVY0MM}w*#H0l literal 0 HcmV?d00001 diff --git a/test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.1.0.zip b/test-registries/primary-registry/contents/diamond-graph/indirect-dependency-a/0.1.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..1477428d3c029f0d935ce1d909357c20b0a3b4d1 GIT binary patch literal 842 zcmWIWW@h1H00GVUC&IuCD8a%Y!%$q5tREV}$-sO!t0@J9ODnh;7+GF0GcbUO0HAIW zpn47lwFM`_j4b(j6o5P>Am)MU&dkd!(aR}K1e=bWUb>pPr-D7S&wA^fJahi? zg(r`6nmA@kNER%8wzPne0bz><&=x1YcTm9@&$SLnLzM-3f0#2RrKB?p7t9Q}YQ`h6Hx93^!bys{tE-|js(DY(m zx#HQQCm}(a9y%u(H1+ko!X7QB&dE=Vom38V$_yap0y?KWF(;=|uOvS= z2keN&*d1Yd#*pujfq=`!d&@iSSaY&JU=S%O=}1cLKhsr_{5HyCU#&q?(Y$@n;xzYZ zJbu{1we0f~;ozI9t!qN&b9*Pb<|r<75>ol*aBuIzW4n9m=K6nrb@!NH(*5?=PoK?g z+GaG((f(`0nQL44vp0y<9oVo`OZ?Km+i5`ZRxQUV?mG22ksWaXub-aA_F8X@vdYtw4JnOygif_mz##I`c zUaTuuJbUycBuLXk=OlxszMfauqep9=u3|-U&K96^v>I%p%Yjar0mNKD=aeVr;dkO!j)S$pd+cK+FTxotc+eqL)*e2sUL~Hda%#y>vBoPX&8upY_%|dFK4( z3r`;DG;z$7C|vq%X#pbx!V(^!B~EBYKo z#j{6GLV`3sbWSp8>g#!hJ$kg}=_*zv=kNoaW7iN_Qx0^>3?SwLI;T7_C#O=cBtJI? z?1*{T9btNABNvmQK+8ir|H38b?U8V>mA%$Lr!z9iG2@DE37{81K!D*bD1f1n&kBir zjED!Bge&SHCNTodSkh<*H3^XrfF^&dc@1roNt6*rP{lo~~j=a?VPi zbE*?9ww42(G6RUYfX*pT%*m)nD0+pl*_`~1=PyVploNq+SZgSqPSXPi>gOj-IXCPGL%iJMb&lEOyz zPkHT9`9{AE*Uaj_w^l~+TkVdxynC7%y2+AXg85J6`2OC&mi>^OCwf(p;@kRH_x3Ud zcr!A|G2==M5=0.1.0, <0.2.0"},"dev-dependencies":{}} +{"package":{"name":"diamond-graph/direct-dependency-a","version":"0.1.1","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{"Indirect":"diamond-graph/indirect-dependency-a@>=0.1.1, <0.2.0"},"dev-dependencies":{}} \ No newline at end of file diff --git a/test-registries/primary-registry/index/diamond-graph/direct-dependency-b b/test-registries/primary-registry/index/diamond-graph/direct-dependency-b new file mode 100644 index 00000000..e29c11fb --- /dev/null +++ b/test-registries/primary-registry/index/diamond-graph/direct-dependency-b @@ -0,0 +1 @@ +{"package":{"name":"diamond-graph/direct-dependency-b","version":"0.1.0","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{"Indirect":"diamond-graph/indirect-dependency-a@>=0.2.0, <0.3.0"},"dev-dependencies":{}} \ No newline at end of file diff --git a/test-registries/primary-registry/index/diamond-graph/indirect-dependency-a b/test-registries/primary-registry/index/diamond-graph/indirect-dependency-a new file mode 100644 index 00000000..5cc762d5 --- /dev/null +++ b/test-registries/primary-registry/index/diamond-graph/indirect-dependency-a @@ -0,0 +1,4 @@ +{"package":{"name":"diamond-graph/indirect-dependency-a","version":"0.2.0","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{},"dev-dependencies":{}} +{"package":{"name":"diamond-graph/indirect-dependency-a","version":"0.2.1","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{},"dev-dependencies":{}} +{"package":{"name":"diamond-graph/indirect-dependency-a","version":"0.1.0","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{},"dev-dependencies":{}} +{"package":{"name":"diamond-graph/indirect-dependency-a","version":"0.1.1","registry":"test-registries/primary-registry","realm":"server","description":null,"license":"MIT","authors":[],"include":[],"exclude":[],"private":false},"place":{"shared-packages":null,"server-packages":null},"dependencies":{},"server-dependencies":{},"dev-dependencies":{}} \ No newline at end of file diff --git a/tests/integration/install.rs b/tests/integration/install.rs index 795df36f..35f37b01 100644 --- a/tests/integration/install.rs +++ b/tests/integration/install.rs @@ -1,9 +1,6 @@ -use std::path::{Path, PathBuf}; - -use fs_err as fs; +use super::temp_project::TempProject; use libwally::{Args, GlobalOptions, InstallSubcommand, Subcommand}; -use tempfile::{tempdir, TempDir}; -use walkdir::WalkDir; +use std::path::Path; #[test] fn minimal() { @@ -66,54 +63,3 @@ fn run_test(name: &str) -> TempProject { assert_dir_snapshot!(project.path()); project } - -/// A handle to a project contained in a temporary directory. The project is -/// deleted when this type is dropped, which happens at the end of a test. -struct TempProject { - dir: TempDir, -} - -impl TempProject { - fn new(source: &Path) -> anyhow::Result { - let dir = tempdir()?; - copy_dir_all(source, dir.path())?; - - Ok(Self { dir }) - } - - fn path(&self) -> &Path { - self.dir.path() - } - - /// Leak our reference to the temporary directory, allowing it to persist - /// after the test runs. Useful for debugging. - #[allow(unused)] - fn leak(self) -> PathBuf { - let path = self.dir.path().to_owned(); - let dir = Box::new(self.dir); - Box::leak(dir); - - path - } -} - -/// Copy the contents of a directory into another directory. Because we use this -/// function with temp directories, the destination directory is expected to -/// already exist. -fn copy_dir_all(from: &Path, into: &Path) -> anyhow::Result<()> { - let source = WalkDir::new(from).min_depth(1).follow_links(true); - - for entry in source { - let entry = entry?; - let relative_path = entry.path().strip_prefix(from).unwrap(); - let dest_path = into.join(relative_path); - - if entry.file_type().is_dir() { - fs::create_dir(&dest_path)?; - } else { - fs::copy(entry.path(), dest_path)?; - } - } - - Ok(()) -} diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 18c5b85f..5d9b0538 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1,6 +1,8 @@ #[macro_use] mod util; +mod temp_project; mod install; mod publish; mod read_projects; +mod update; diff --git a/tests/integration/snapshots/integration__update__update_all_dependencies.snap b/tests/integration/snapshots/integration__update__update_all_dependencies.snap new file mode 100644 index 00000000..3eef4d53 --- /dev/null +++ b/tests/integration/snapshots/integration__update__update_all_dependencies.snap @@ -0,0 +1,33 @@ +--- +source: tests/integration/update.rs +expression: lockfile +--- +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.1" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.1"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.1"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.1"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] + diff --git a/tests/integration/snapshots/integration__update__update_list_of_specs.snap b/tests/integration/snapshots/integration__update__update_list_of_specs.snap new file mode 100644 index 00000000..6872c1b1 --- /dev/null +++ b/tests/integration/snapshots/integration__update__update_list_of_specs.snap @@ -0,0 +1,33 @@ +--- +source: tests/integration/update.rs +expression: lockfile_content +--- +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.1" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.1"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.1"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.1"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] + diff --git a/tests/integration/snapshots/integration__update__update_named_dependency.snap b/tests/integration/snapshots/integration__update__update_named_dependency.snap new file mode 100644 index 00000000..0394c4d0 --- /dev/null +++ b/tests/integration/snapshots/integration__update__update_named_dependency.snap @@ -0,0 +1,33 @@ +--- +source: tests/integration/update.rs +expression: lockfile_contents +--- +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.1"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.1"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.0"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] + diff --git a/tests/integration/snapshots/integration__update__update_required_dependency.snap b/tests/integration/snapshots/integration__update__update_required_dependency.snap new file mode 100644 index 00000000..2cb54c15 --- /dev/null +++ b/tests/integration/snapshots/integration__update__update_required_dependency.snap @@ -0,0 +1,33 @@ +--- +source: tests/integration/update.rs +expression: lockfile_content +--- +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "diamond-graph/direct-dependency-a" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.1.1"]] + +[[package]] +name = "diamond-graph/direct-dependency-b" +version = "0.1.0" +dependencies = [["Indirect", "diamond-graph/indirect-dependency-a@0.2.0"]] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.1.1" +dependencies = [] + +[[package]] +name = "diamond-graph/indirect-dependency-a" +version = "0.2.0" +dependencies = [] + +[[package]] +name = "diamond-graph/root" +version = "0.1.0" +dependencies = [["A", "diamond-graph/direct-dependency-a@0.1.0"], ["B", "diamond-graph/direct-dependency-b@0.1.0"]] + diff --git a/tests/integration/temp_project.rs b/tests/integration/temp_project.rs new file mode 100644 index 00000000..a46022f4 --- /dev/null +++ b/tests/integration/temp_project.rs @@ -0,0 +1,55 @@ +use fs_err as fs; +use std::path::{Path, PathBuf}; +use tempfile::{tempdir, TempDir}; +use walkdir::WalkDir; + +/// A handle to a project contained in a temporary directory. The project is +/// deleted when this type is dropped, which happens at the end of a test. +pub struct TempProject { + dir: TempDir, +} + +impl TempProject { + pub fn new(source: &Path) -> anyhow::Result { + let dir = tempdir()?; + copy_dir_all(source, dir.path())?; + + Ok(Self { dir }) + } + + pub fn path(&self) -> &Path { + self.dir.path() + } + + /// Leak our reference to the temporary directory, allowing it to persist + /// after the test runs. Useful for debugging. + #[allow(unused)] + pub fn leak(self) -> PathBuf { + let path = self.dir.path().to_owned(); + let dir = Box::new(self.dir); + Box::leak(dir); + + path + } +} + +/// Copy the contents of a directory into another directory. Because we use this +/// function with temp directories, the destination directory is expected to +/// already exist. +fn copy_dir_all(from: &Path, into: &Path) -> anyhow::Result<()> { + let source = WalkDir::new(from).min_depth(1).follow_links(true); + + for entry in source { + let entry = entry?; + let relative_path = entry.path().strip_prefix(from).unwrap(); + let dest_path = into.join(relative_path); + + if entry.file_type().is_dir() { + fs::create_dir(&dest_path)?; + } else { + fs::copy(entry.path(), dest_path)?; + } + } + + Ok(()) +} diff --git a/tests/integration/update.rs b/tests/integration/update.rs new file mode 100644 index 00000000..48db94da --- /dev/null +++ b/tests/integration/update.rs @@ -0,0 +1,152 @@ +use crate::temp_project::TempProject; +use fs_err as fs; +use insta::assert_snapshot; +use libwally::{ + package_name::PackageName, package_req::PackageReq, Args, GlobalOptions, PackageSpec, + Subcommand, UpdateSubcommand, +}; +use std::{path::Path, str::FromStr}; + +#[test] +fn generate_new_lockfile_if_missing() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/fresh" + )); + + let project = TempProject::new(&source_project).unwrap(); + + let result = run_update(&project); + + assert!(result.is_ok(), "It should of ran without any errors."); + + assert!( + fs::metadata(project.path().join("wally.lock")) + .unwrap() + .is_file(), + "It should've of generated a new wally.lock file." + ) +} + +#[test] +fn install_new_packages_after_updating() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/fresh" + )); + + let project = TempProject::new(&source_project).unwrap(); + + let result = run_update(&project); + + assert!(result.is_ok(), "It should of ran without any errors."); + + assert!( + fs::metadata(project.path().join("ServerPackages")) + .unwrap() + .is_dir(), + "ServerPackages was made and has its dependencies." + ) +} + +#[test] +fn update_all_dependencies() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/dated" + )); + + let project = TempProject::new(&source_project).unwrap(); + + run_update(&project).unwrap(); + + let lockfile = fs::read_to_string(project.path().join("wally.lock")).unwrap(); + assert_snapshot!(lockfile); +} + +#[test] +/// It should update both indirect-dependency-a v0.1.0 and 0.2.0 to v0.1.1 and 0.2.1 respectively +fn update_named_dependency() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/dated" + )); + + let project = TempProject::new(&source_project).unwrap(); + + run_update_with_specs( + &project, + vec![PackageSpec::Named( + PackageName::new("diamond-graph", "indirect-dependency-a").unwrap(), + )], + ) + .unwrap(); + + let lockfile_contents = fs::read_to_string(project.path().join("wally.lock")).unwrap(); + assert_snapshot!(lockfile_contents); +} + +#[test] +/// It should only update @0.1.0 instead of the @0.2.0 version. +fn update_required_dependency() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/dated" + )); + + let project = TempProject::new(&source_project).unwrap(); + + run_update_with_specs( + &project, + vec![PackageSpec::Required( + PackageReq::from_str("diamond-graph/indirect-dependency-a@0.1.0".into()).unwrap(), + )], + ) + .unwrap(); + + let lockfile_content = fs::read_to_string(project.path().join("wally.lock")).unwrap(); + assert_snapshot!(lockfile_content); +} + +#[test] +/// It should update everything that can be updated, in a round-about way. +fn update_list_of_specs() { + let source_project = Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-projects/diamond-graph/root/dated" + )); + + let project = TempProject::new(&source_project).unwrap(); + + run_update_with_specs( + &project, + vec![ + PackageSpec::Required( + PackageReq::from_str("diamond-graph/direct-dependency-a@0.1.0".into()).unwrap(), + ), + PackageSpec::Named(PackageName::new("diamond-graph", "indirect-dependency-a").unwrap()), + ], + ) + .unwrap(); + + let lockfile_content = fs::read_to_string(project.path().join("wally.lock")).unwrap(); + assert_snapshot!(lockfile_content); +} + +fn run_update(project: &TempProject) -> anyhow::Result<()> { + run_update_with_specs(project, Vec::new()) +} + +fn run_update_with_specs(project: &TempProject, specs: Vec) -> anyhow::Result<()> { + Args { + global: GlobalOptions { + test_registry: true, + ..Default::default() + }, + subcommand: Subcommand::Update(UpdateSubcommand { + project_path: project.path().to_owned(), + package_specs: specs, + }), + } + .run() +} From 6d434c842f7c35d9a721f239211d29094ace437c Mon Sep 17 00:00:00 2001 From: magnalite Date: Tue, 13 Jun 2023 03:44:58 +0100 Subject: [PATCH 08/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f503e1e..d2c28979 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Installs all packages. Parity with: * `npm install` with no arguments -### `wally update [package-names]` (unimplemented) +### `wally update [package-names]` Update packages recursively. By default, will update all packages. If any package names are given (in the form `scope/name` or `scope/name@version-req`), just those packages will be updated instead. Parity with: From 6f890796c3d13049e6b24f8295591734d8cc8b5e Mon Sep 17 00:00:00 2001 From: magnalite Date: Tue, 13 Jun 2023 16:06:07 +0100 Subject: [PATCH 09/13] Fix nightly builds (#152) * Initial copy of upload action * Pivot to svenstaro/upload-release-action * Finalise nightly text --- .github/workflows/build.yml | 58 ++++++++++++++++++++++++----------- .github/workflows/nightly.yml | 10 +++++- .github/workflows/release.yml | 4 ++- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cce26efa..818a0121 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ on: workflow_call: inputs: + tag: + required: true + type: string + upload_url: required: true type: string @@ -9,6 +13,18 @@ on: required: true type: string + overwrite: + required: true + type: boolean + + release_name: + required: false + type: string + + release_body: + required: false + type: string + jobs: windows: runs-on: windows-latest @@ -32,14 +48,16 @@ jobs: 7z a ../release.zip * - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: svenstaro/upload-release-action@v2 with: - upload_url: ${{ inputs.upload_url }} - asset_path: release.zip + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ inputs.tag }} + file: release.zip asset_name: wally-${{ inputs.release_ref }}-win64.zip - asset_content_type: application/octet-stream + overwrite: ${{ inputs.overwrite }} + release_name: ${{ inputs.release_name }} + body: ${{ inputs.release_body }} + prerelease: true macos: runs-on: macos-latest @@ -70,14 +88,16 @@ jobs: zip ../release.zip * - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: svenstaro/upload-release-action@v2 with: - upload_url: ${{ inputs.upload_url }} - asset_path: release.zip + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ inputs.tag }} + file: release.zip asset_name: wally-${{ inputs.release_ref }}-macos.zip - asset_content_type: application/octet-stream + overwrite: ${{ inputs.overwrite }} + release_name: ${{ inputs.release_name }} + body: ${{ inputs.release_body }} + prerelease: true linux: runs-on: ubuntu-latest @@ -103,11 +123,13 @@ jobs: zip ../release.zip * - name: Upload Archive to Release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: svenstaro/upload-release-action@v2 with: - upload_url: ${{ inputs.upload_url }} - asset_path: release.zip + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ inputs.tag }} + file: release.zip asset_name: wally-${{ inputs.release_ref }}-linux.zip - asset_content_type: application/octet-stream \ No newline at end of file + overwrite: ${{ inputs.overwrite }} + release_name: ${{ inputs.release_name }} + body: ${{ inputs.release_body }} + prerelease: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5722802c..f997d45a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,5 +36,13 @@ jobs: uses: ./.github/workflows/build.yml needs: fetch_tag with: + tag: nightly upload_url: https://uploads.github.com/repos/UpliftGames/wally/releases/108261310/assets{?name,label} - release_ref: ${{ needs.fetch_tag.outputs.previous_tag }}-nightly \ No newline at end of file + release_ref: ${{ needs.fetch_tag.outputs.previous_tag }}-nightly + overwrite: true + release_name: "Nightly 🌙 " + release_body: | + ### For those who live life on the edge 🔪🩸😎 + + ⚠️ Not suitable for production use + 🚀 Built from latest main commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcd78056..ed83b9ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,5 +26,7 @@ jobs: uses: ./.github/workflows/build.yml needs: create-release with: + tag: ${{ github.ref_name }} upload_url: ${{ needs.create-release.outputs.upload_url }} - release_ref: ${{ github.ref_name }} \ No newline at end of file + release_ref: ${{ github.ref_name }} + overwrite: false \ No newline at end of file From 0d4a3d3e19084d2704d017d3ccf3b64db312bcea Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Jun 2023 21:05:34 -0700 Subject: [PATCH 10/13] Fix downloaded file name having no type extension (#155) --- wally-registry-frontend/src/pages/Package.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wally-registry-frontend/src/pages/Package.tsx b/wally-registry-frontend/src/pages/Package.tsx index 5c6dfef8..8665bc82 100644 --- a/wally-registry-frontend/src/pages/Package.tsx +++ b/wally-registry-frontend/src/pages/Package.tsx @@ -393,7 +393,8 @@ export default function Package() { "/" + packageName + "@" + - packageMetadata.package.version + packageMetadata.package.version + + ".zip" } > Date: Wed, 14 Jun 2023 15:39:59 +0100 Subject: [PATCH 11/13] Update CONTRIBUTING.md --- CONTRIBUTING.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a437480..7948d0e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,23 @@ # Contribution guide for wally -More info will be added here in the future. For now, if you want changes then please add an issue or create a PR! All issues, PRs, and comments on PRs are incredibly helpful. It may take us a while to get to your PR but if you feel it is important then please head to the #wally channel in the Roblox OSS discord server (find this in the top right of [wally.run](https://wally.run/)) and give us a ping! +Please respect that wally is still in early stages of development. Things are messy. Time is precious. We may be slow. + +- **Only start making a PR if you are confident with Rust or TS/React** (issues and comments are always appreciated though) +- **Documentation for developing exists but may be outdated** + - **Be prepared to figure out how to get things going** + - **Fixing up any issues you do have getting going is a fantastic way to start helping!** +- **Polish and refine PRs as much as possible and ensure they pass CI checks before requesting a review** +- **If you want advise on a draft then discuss it in [#wally] first.** + +Beyond that we are pretty chill, I promise! If you make good changes I will do my best to help you get them to the finish line. + +More info will be added here in the future. For now, if you want changes then please add an issue or create a PR! All issues, PRs, and comments on PRs are incredibly helpful. It may take us a while to get to your PR but if you feel it is important then please head to the [#wally] channel in the Roblox OSS discord server (find this in the top right of [wally.run](https://wally.run/)) and let us know! + +The current lead maintainer for wally is @magnalite, that's me! If you want to work on a complex change or feel like your pr/issue has gone unnoticed for too long then give me a ping in [#wally]! + +Finally, as you may have guessed by now... If in doubt head to [#wally] and ask. Anyone is welcome to come in and ask anything about wally. + +[#wally]: https://discord.com/channels/385151591524597761/872225914149302333 ## Creating a new wally release @@ -20,4 +37,4 @@ More info will be added here in the future. For now, if you want changes then pl 7. Push `git push && git push --tags` 8. Update release information -(Thank you to lpg / rojo for inspiration for this release checklist) \ No newline at end of file +(Thank you to lpg / rojo for inspiration for this release checklist) From 1b9f99f5043e6463ad898094ed74a0722b1c8e8b Mon Sep 17 00:00:00 2001 From: magnalite Date: Tue, 11 Jul 2023 15:44:47 +0100 Subject: [PATCH 12/13] Add status badge to README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2c28979..5c068e7f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@
Wally Logo -

Wally, a package manager for Roblox

+

Wally, a package manager for Roblox +
Dynamic Status Badge +

+ * [Installation](#installation) * [Commands](#commands) * [Prior Art](#prior-art) From 4434c1f4acde7aeaf60bc4ffd4295bbfb626d7c0 Mon Sep 17 00:00:00 2001 From: magnalite Date: Tue, 11 Jul 2023 15:49:15 +0100 Subject: [PATCH 13/13] Update status badge in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c068e7f..312d5545 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Wally Logo

Wally, a package manager for Roblox -
Dynamic Status Badge +
Dynamic Status Badge