From 1dd34c8f5c9bd388e7e22bdb2a17f45e6b766d5b Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 18 Nov 2024 11:59:45 +0400 Subject: [PATCH 01/13] [backend] multiple owners/maintainers --- backend/main/PackagePublisher.mo | 25 ++--- backend/main/main-canister.mo | 104 ++++++++++++++----- backend/main/registry/Registry.mo | 114 ++++++++++++++++++--- backend/main/registry/getPackageSummary.mo | 13 ++- backend/main/registry/searchInRegistry.mo | 16 +-- backend/main/types.mo | 5 +- 6 files changed, 212 insertions(+), 65 deletions(-) diff --git a/backend/main/PackagePublisher.mo b/backend/main/PackagePublisher.mo index aa57e09d..2b5f2da3 100644 --- a/backend/main/PackagePublisher.mo +++ b/backend/main/PackagePublisher.mo @@ -67,19 +67,16 @@ module { }; // check permissions - switch (registry.getPackageOwner(config.name)) { - case (null) { - // deny '.' and '_' in name for new packages - for (char in config.name.chars()) { - let err = #err("invalid config: unexpected char '" # Char.toText(char) # "' in name '" # config.name # "'"); - if (char == '.' or char == '_') { - return err; - }; - }; - }; - case (?owner) { - if (owner != caller) { - return #err("You don't have permissions to publish this package"); + if (not registry.isOwner(config.name, caller) and not registry.isMaintainer(config.name, caller)) { + return #err("Only owners and maintainers can publish packages"); + }; + + // deny '.' and '_' in name for new packages + if (registry.getHighestVersion(config.name) == null) { + for (char in config.name.chars()) { + let err = #err("invalid config: unexpected char '" # Char.toText(char) # "' in name '" # config.name # "'"); + if (char == '.' or char == '_') { + return err; }; }; }; @@ -103,7 +100,7 @@ module { if (dep.repo.size() == 0 and registry.getPackageConfig(PackageUtils.getDepName(dep.name), dep.version) == null) { return #err("Dependency " # packageId # " not found in registry"); }; - if (dep.repo.size() != 0 and registry.getPackageOwner(PackageUtils.getDepName(dep.name)) != null) { + if (dep.repo.size() != 0 and registry.getHighestVersion(PackageUtils.getDepName(dep.name)) != null) { return #err("GitHub dependency `" # dep.name # "` conflicts with an existing package in the Mops registry. Please change the dependency name in mops.toml file."); }; }; diff --git a/backend/main/main-canister.mo b/backend/main/main-canister.mo index 24504d11..5361e61f 100644 --- a/backend/main/main-canister.mo +++ b/backend/main/main-canister.mo @@ -62,7 +62,9 @@ actor class Main() { let API_VERSION = "1.3"; // (!) make changes in pair with cli var packageVersions = TrieMap.TrieMap(Text.equal, Text.hash); - var packageOwners = TrieMap.TrieMap(Text.equal, Text.hash); + var packageOwners = TrieMap.TrieMap(Text.equal, Text.hash); // legacy + var ownersByPackage = TrieMap.TrieMap(Text.equal, Text.hash); + var maintainersByPackage = TrieMap.TrieMap(Text.equal, Text.hash); var highestConfigs = TrieMap.TrieMap(Text.equal, Text.hash); var packageConfigs = TrieMap.TrieMap(Text.equal, Text.hash); @@ -76,7 +78,8 @@ actor class Main() { var registry = Registry.Registry( packageVersions, - packageOwners, + ownersByPackage, + maintainersByPackage, highestConfigs, packageConfigs, packagePublications, @@ -489,8 +492,28 @@ actor class Main() { users.setUserProp(caller, prop, value); }; - public shared ({caller}) func transferOwnership(packageName : PackageName, newOwner : Principal) : async Result.Result<(), Text> { - registry.transferOwnership(caller, packageName, newOwner); + public query func getPackageOwners(packageName : PackageName) : async [Principal] { + registry.getPackageOwners(packageName); + }; + + public query func getPackageMaintainers(packageName : PackageName) : async [Principal] { + registry.getPackageMaintainers(packageName); + }; + + public shared ({caller}) func addOwner(packageName : PackageName, newOwner : Principal) : async Result.Result<(), Text> { + registry.addOwner(caller, packageName, newOwner); + }; + + public shared ({caller}) func addMaintainer(packageName : PackageName, newMaintainer : Principal) : async Result.Result<(), Text> { + registry.addMaintainer(caller, packageName, newMaintainer); + }; + + public shared ({caller}) func removeOwner(packageName : PackageName, owner : Principal) : async Result.Result<(), Text> { + registry.removeOwner(caller, packageName, owner); + }; + + public shared ({caller}) func removeMaintainer(packageName : PackageName, maintainer : Principal) : async Result.Result<(), Text> { + registry.removeMaintainer(caller, packageName, maintainer); }; // BADGES @@ -526,10 +549,11 @@ actor class Main() { let backupManager = Backup.BackupManager(backupStateV2, {maxBackups = 20}); type BackupChunk = { - #v7 : { + #v8 : { #packagePublications : [(PackageId, PackagePublication)]; #packageVersions : [(PackageName, [PackageVersion])]; - #packageOwners : [(PackageName, Principal)]; + #ownersByPackage : [(PackageName, [Principal])]; + #maintainersByPackage : [(PackageName, [Principal])]; #packageConfigs : [(PackageId, PackageConfigV3)]; #highestConfigs : [(PackageName, PackageConfigV3)]; #fileIdsByPackage : [(PackageId, [FileId])]; @@ -555,22 +579,23 @@ actor class Main() { }; func _backup() : async () { - let backup = backupManager.NewBackup("v7"); + let backup = backupManager.NewBackup("v8"); await backup.startBackup(); - await backup.uploadChunk(to_candid(#v7(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageOwners(Iter.toArray(packageOwners.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageFileStats(Iter.toArray(packageFileStats.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageBenchmarks(Iter.toArray(packageBenchmarks.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#downloadLog(downloadLog.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#storageManager(storageManager.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#users(users.toStable())) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk)); - await backup.uploadChunk(to_candid(#v7(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packagePublications(Iter.toArray(packagePublications.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageVersions(Iter.toArray(packageVersions.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#ownersByPackage(Iter.toArray(ownersByPackage.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#maintainersByPackage(Iter.toArray(maintainersByPackage.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#fileIdsByPackage(Iter.toArray(fileIdsByPackage.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#hashByFileId(Iter.toArray(hashByFileId.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageFileStats(Iter.toArray(packageFileStats.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageTestStats(Iter.toArray(packageTestStats.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageBenchmarks(Iter.toArray(packageBenchmarks.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageNotes(Iter.toArray(packageNotes.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#downloadLog(downloadLog.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#storageManager(storageManager.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#users(users.toStable())) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#highestConfigs(Iter.toArray(highestConfigs.entries()))) : BackupChunk)); + await backup.uploadChunk(to_candid(#v8(#packageConfigs(Iter.toArray(packageConfigs.entries()))) : BackupChunk)); await backup.finishBackup(); }; @@ -580,7 +605,7 @@ actor class Main() { assert(Utils.isAdmin(caller)); await backupManager.restore(backupId, func(blob : Blob) { - let ?#v7(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk"); + let ?#v8(chunk) : ?BackupChunk = from_candid(blob) else Debug.trap("Failed to restore chunk"); switch (chunk) { case (#packagePublications(packagePublicationsStable)) { @@ -589,8 +614,11 @@ actor class Main() { case (#packageVersions(packageVersionsStable)) { packageVersions := TrieMap.fromEntries(packageVersionsStable.vals(), Text.equal, Text.hash); }; - case (#packageOwners(packageOwnersStable)) { - packageOwners := TrieMap.fromEntries(packageOwnersStable.vals(), Text.equal, Text.hash); + case (#ownersByPackage(ownersByPackageStable)) { + ownersByPackage := TrieMap.fromEntries(ownersByPackageStable.vals(), Text.equal, Text.hash); + }; + case (#maintainersByPackage(maintainersByPackageStable)) { + maintainersByPackage := TrieMap.fromEntries(maintainersByPackageStable.vals(), Text.equal, Text.hash); }; case (#fileIdsByPackage(fileIdsByPackageStable)) { fileIdsByPackage := TrieMap.fromEntries(fileIdsByPackageStable.vals(), Text.equal, Text.hash); @@ -634,7 +662,8 @@ actor class Main() { // re-init registry registry := Registry.Registry( packageVersions, - packageOwners, + ownersByPackage, + maintainersByPackage, highestConfigs, packageConfigs, packagePublications, @@ -652,6 +681,8 @@ actor class Main() { stable var packagePublicationsStable : [(PackageId, PackagePublication)] = []; stable var packageVersionsStable : [(PackageName, [PackageVersion])] = []; stable var packageOwnersStable : [(PackageName, Principal)] = []; + stable var ownersByPackageStable : [(PackageName, [Principal])] = []; + stable var maintainersByPackageStable : [(PackageName, [Principal])] = []; stable var packageConfigsStableV3 : [(PackageId, PackageConfigV3)] = []; stable var highestConfigsStableV3 : [(PackageName, PackageConfigV3)] = []; @@ -671,6 +702,8 @@ actor class Main() { packagePublicationsStable := Iter.toArray(packagePublications.entries()); packageVersionsStable := Iter.toArray(packageVersions.entries()); packageOwnersStable := Iter.toArray(packageOwners.entries()); + ownersByPackageStable := Iter.toArray(ownersByPackage.entries()); + maintainersByPackageStable := Iter.toArray(maintainersByPackage.entries()); fileIdsByPackageStable := Iter.toArray(fileIdsByPackage.entries()); hashByFileIdStable := Iter.toArray(hashByFileId.entries()); packageFileStatsStable := Iter.toArray(packageFileStats.entries()); @@ -698,6 +731,24 @@ actor class Main() { packageVersions := TrieMap.fromEntries(packageVersionsStable.vals(), Text.equal, Text.hash); packageVersionsStable := []; + // migrate packageOwners -> ownersByPackage + if (ownersByPackageStable.size() == 0) { + ownersByPackage := + packageOwnersStable.vals() + |> Iter.map<(PackageName, Principal), (PackageName, [Principal])>(_, func((name, owner)) = (name, [owner])) + |> TrieMap.fromEntries(_, Text.equal, Text.hash); + } + else { + ownersByPackage := TrieMap.fromEntries(ownersByPackageStable.vals(), Text.equal, Text.hash); + ownersByPackageStable := []; + }; + + ownersByPackage := TrieMap.fromEntries(ownersByPackageStable.vals(), Text.equal, Text.hash); + ownersByPackageStable := []; + + maintainersByPackage := TrieMap.fromEntries(maintainersByPackageStable.vals(), Text.equal, Text.hash); + maintainersByPackageStable := []; + packageOwners := TrieMap.fromEntries(packageOwnersStable.vals(), Text.equal, Text.hash); packageOwnersStable := []; @@ -732,7 +783,8 @@ actor class Main() { registry := Registry.Registry( packageVersions, - packageOwners, + ownersByPackage, + maintainersByPackage, highestConfigs, packageConfigs, packagePublications, diff --git a/backend/main/registry/Registry.mo b/backend/main/registry/Registry.mo index 3e08a3a0..7492e36a 100644 --- a/backend/main/registry/Registry.mo +++ b/backend/main/registry/Registry.mo @@ -4,6 +4,7 @@ import Option "mo:base/Option"; import Array "mo:base/Array"; import Time "mo:base/Time"; import Result "mo:base/Result"; +import Principal "mo:base/Principal"; import Types "../types"; import Semver "../utils/semver"; @@ -34,7 +35,8 @@ module { public class Registry( packageVersions : TrieMap.TrieMap, - packageOwners : TrieMap.TrieMap, + ownersByPackage : TrieMap.TrieMap, + maintainersByPackage : TrieMap.TrieMap, highestConfigs : TrieMap.TrieMap, packageConfigs : TrieMap.TrieMap, packagePublications : TrieMap.TrieMap, @@ -59,7 +61,13 @@ module { packageVersions.put(newRelease.config.name, Array.append(versions, [newRelease.config.version])); packageConfigs.put(packageId, newRelease.config); - packageOwners.put(newRelease.config.name, newRelease.userId); + + // add owner for new package + let owners = getPackageOwners(newRelease.config.name); + if (owners.size() == 0) { + ownersByPackage.put(newRelease.config.name, [newRelease.userId]); + }; + packagePublications.put(packageId, { user = newRelease.userId; time = Time.now(); @@ -119,10 +127,6 @@ module { // By package name // ----------------------------- - public func getPackageOwner(name : PackageName) : ?Principal { - packageOwners.get(name); - }; - public func getPackageVersions(name : PackageName) : ?[PackageVersion] { packageVersions.get(name); }; @@ -169,17 +173,101 @@ module { // Package ownership // ----------------------------- - public func transferOwnership(caller : Principal, packageName : PackageName, newOwner : Principal) : Result.Result<(), Text> { - let ?oldOwner = packageOwners.get(packageName) else return #err("Package not found"); + public func getPackageOwners(name : PackageName) : [Principal] { + Option.get(ownersByPackage.get(name), []); + }; + + public func getPackageMaintainers(name : PackageName) : [Principal] { + Option.get(maintainersByPackage.get(name), []); + }; + + public func isOwner(name : PackageName, principal : Principal) : Bool { + for (owner in getPackageOwners(name).vals()) { + if (owner == principal) { + return true; + }; + }; + return false; + }; + + public func isMaintainer(name : PackageName, principal : Principal) : Bool { + for (maintainer in getPackageMaintainers(name).vals()) { + if (maintainer == principal) { + return true; + }; + }; + return false; + }; + + public func addOwner(caller : Principal, packageName : PackageName, newOwner : Principal) : Result.Result<(), Text> { + let ?owners = ownersByPackage.get(packageName) else return #err("Package not found"); + + if (isOwner(packageName, newOwner)) { + return #err("User is already an owner"); + }; + if (not isOwner(packageName, caller)) { + return #err("Only owners can add owners"); + }; + if (owners.size() >= 5) { + return #err("Maximum number of owners reached"); + }; + + ownersByPackage.put(packageName, Array.append(owners, [newOwner])); + #ok; + }; + + public func addMaintainer(caller : Principal, packageName : PackageName, newMaintainer : Principal) : Result.Result<(), Text> { + let ?maintainers = maintainersByPackage.get(packageName) else return #err("Package not found"); + + if (isMaintainer(packageName, newMaintainer)) { + return #err("User is already a maintainer"); + }; + if (not isOwner(packageName, caller)) { + return #err("Only owners can add maintainers"); + }; + if (maintainers.size() >= 5) { + return #err("Maximum number of maintainers reached"); + }; + + maintainersByPackage.put(packageName, Array.append(maintainers, [newMaintainer])); + #ok; + }; - if (oldOwner != caller) { - return #err("Only owner can transfer ownership"); + public func removeOwner(caller : Principal, packageName : PackageName, ownerToRemove : Principal) : Result.Result<(), Text> { + let ?owners = ownersByPackage.get(packageName) else return #err("Package not found"); + + if (not isOwner(packageName, ownerToRemove)) { + return #err("User is not an owner"); + }; + if (not isOwner(packageName, caller)) { + return #err("Only owners can remove owners"); + }; + if (owners.size() <= 1) { + return #err("At least one owner is required"); + }; + + ownersByPackage.put(packageName, Array.filter(owners, func(owner : Principal) : Bool { + owner != ownerToRemove; + })); + #ok; + }; + + public func removeMaintainer(caller : Principal, packageName : PackageName, maintainerToRemove : Principal) : Result.Result<(), Text> { + let ?maintainers = maintainersByPackage.get(packageName) else return #err("Package not found"); + + if (not isMaintainer(packageName, maintainerToRemove)) { + return #err("User is not a maintainer"); + }; + if (not isOwner(packageName, caller)) { + return #err("Only owners can remove maintainers"); }; - if (newOwner == caller) { - return #err("You can't transfer ownership to yourself"); + if (maintainers.size() <= 1) { + return #err("At least one maintainer is required"); }; - packageOwners.put(packageName, newOwner); + maintainersByPackage.put(packageName, Array.filter(maintainers, func(maintainer : Principal) : Bool { + maintainer != maintainerToRemove; + })); #ok; }; }; diff --git a/backend/main/registry/getPackageSummary.mo b/backend/main/registry/getPackageSummary.mo index a5fd793e..77f03c89 100644 --- a/backend/main/registry/getPackageSummary.mo +++ b/backend/main/registry/getPackageSummary.mo @@ -1,6 +1,7 @@ import Time "mo:base/Time"; import Option "mo:base/Option"; import Debug "mo:base/Debug"; +import Array "mo:base/Array"; import {DAY} "mo:time-consts"; import Types "../types"; @@ -25,15 +26,19 @@ module { let config = registry.getPackageConfig(name, version)!; let publication = registry.getPackagePublication(name, version)!; - let owner = registry.getPackageOwner(name)!; - users.ensureUser(owner); + let owners = registry.getPackageOwners(name); + for (owner in owners.vals()) { + users.ensureUser(owner); + }; + let owner = owners[0]; let highestVersion = registry.getHighestVersion(name)!; return ?{ depAlias = ""; - owner = owner; - ownerInfo = users.getUser(owner); + owner = owner; // legacy + ownerInfo = users.getUser(owner); // legacy + owners = Array.map(owners, users.getUser); config = config; publication = publication; downloadsInLast7Days = downloadLog.getDownloadsByPackageNameIn(config.name, 7 * DAY, Time.now()); diff --git a/backend/main/registry/searchInRegistry.mo b/backend/main/registry/searchInRegistry.mo index c16e1a7e..7848f396 100644 --- a/backend/main/registry/searchInRegistry.mo +++ b/backend/main/registry/searchInRegistry.mo @@ -41,19 +41,23 @@ module { if (Text.startsWith(searchText, #text("owner:"))) { ignore do ? { let searchOwnerNameOrId = Text.stripStart(searchText, #text("owner:"))!; - let ownerId = registry.getPackageOwner(config.name)!; + let owners = registry.getPackageOwners(config.name); // search by owner id if (Option.isSome(PrincipalExt.fromText(searchOwnerNameOrId))) { - if (searchOwnerNameOrId == Principal.toText(ownerId)) { - sortingPoints += 3; + for (ownerId in owners.vals()) { + if (searchOwnerNameOrId == Principal.toText(ownerId)) { + sortingPoints += 3; + }; }; } // search by owner name else { - let ownerInfo = users.getUserOpt(ownerId)!; - if (searchOwnerNameOrId == ownerInfo.name) { - sortingPoints += 3; + for (ownerId in owners.vals()) { + let ownerInfo = users.getUserOpt(ownerId)!; + if (searchOwnerNameOrId == ownerInfo.name) { + sortingPoints += 3; + }; }; }; }; diff --git a/backend/main/types.mo b/backend/main/types.mo index c4c44b67..028646cf 100644 --- a/backend/main/types.mo +++ b/backend/main/types.mo @@ -89,8 +89,9 @@ module { }; public type PackageSummary = { - owner : Principal; // TODO: ownerId? - ownerInfo : User; + owner : Principal; // legacy + ownerInfo : User; // legacy + owners : [User]; config : PackageConfigV3; publication : PackagePublication; downloadsTotal : Nat; From fc0f5fcdd378bf8d5f1c77fce9023d356b1e15b3 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 18 Nov 2024 12:00:29 +0400 Subject: [PATCH 02/13] [cli] fix `mops watch` when no dfx.json --- cli/commands/watch/parseDfxJson.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/commands/watch/parseDfxJson.ts b/cli/commands/watch/parseDfxJson.ts index 79b56df6..9989cca2 100644 --- a/cli/commands/watch/parseDfxJson.ts +++ b/cli/commands/watch/parseDfxJson.ts @@ -51,14 +51,14 @@ function readDfxJson() : DfxConfig | Record { export function getMotokoCanisters() : Record { let dfxJson = readDfxJson(); - return Object.fromEntries(Object.entries(dfxJson.canisters) + return Object.fromEntries(Object.entries(dfxJson.canisters || {}) .filter(([_, canister]) => canister.type === 'motoko') .map(([name, canister]) => [name, canister.main ?? ''])); } export function getMotokoCanistersWithDeclarations() : Record { let dfxJson = readDfxJson(); - return Object.fromEntries(Object.entries(dfxJson.canisters) + return Object.fromEntries(Object.entries(dfxJson.canisters || {}) .filter(([_, canister]) => canister.type === 'motoko' && canister.declarations) .map(([name, canister]) => [name, canister.main ?? ''])); } \ No newline at end of file From f9c9336684e80040b4aa11bea065a627efd150f4 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 18 Nov 2024 12:01:24 +0400 Subject: [PATCH 03/13] [docs] multiple owners/maintainers --- .../03-package-owners-and-maintainers.md | 20 ++++++++ docs/docs/cli/2-publish/05-mops-owner.md | 50 +++++++++++++++++++ .../2-publish/05-mops-transfer-ownership.md | 35 ------------- docs/docs/cli/2-publish/06-mops-maintainer.md | 50 +++++++++++++++++++ 4 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 docs/docs/articles/03-package-owners-and-maintainers.md create mode 100644 docs/docs/cli/2-publish/05-mops-owner.md delete mode 100644 docs/docs/cli/2-publish/05-mops-transfer-ownership.md create mode 100644 docs/docs/cli/2-publish/06-mops-maintainer.md diff --git a/docs/docs/articles/03-package-owners-and-maintainers.md b/docs/docs/articles/03-package-owners-and-maintainers.md new file mode 100644 index 00000000..31e3747e --- /dev/null +++ b/docs/docs/articles/03-package-owners-and-maintainers.md @@ -0,0 +1,20 @@ +--- +slug: /package-owners-and-maintainers +sidebar_label: Package owners and maintainers +--- + +# Package owners and maintainers + +A package can have multiple owners and maintainers. + +Owners have full control over the package, including the ability to publish new versions, add or remove owners, and maintainers. Maintainers can publish new versions, but they cannot add or remove owners or maintainers. + +| Action | Owner | Maintainer | +|--------|-------|------------| +| Publish new versions | :heavy_check_mark: | :heavy_check_mark: | +| Add or remove owners | :heavy_check_mark: | :x: | +| Add or remove maintainers | :heavy_check_mark: | :x: | + +Use [`mops owner`](/cli/mops-owner) command to manage owners of a package. + +Use [`mops maintainer`](/cli/mops-maintainer) command to manage maintainers of a package. \ No newline at end of file diff --git a/docs/docs/cli/2-publish/05-mops-owner.md b/docs/docs/cli/2-publish/05-mops-owner.md new file mode 100644 index 00000000..38d76f31 --- /dev/null +++ b/docs/docs/cli/2-publish/05-mops-owner.md @@ -0,0 +1,50 @@ +--- +slug: /cli/mops-owner +sidebar_label: mops owner +--- + +# `mops owner` + +List, add or remove owners of a package. + +:::info +Check the [Package owners and maintainers](/package-owners-and-maintainers) for more information. +::: + +## `mops owner` +Manage owners of the current package. + +### `mops owner list` + +List all owners of the package. + +### `mops owner add` + +Add new package owner. +``` +mops owner add +``` + +### `mops owner remove` + +Remove package owner. +``` +mops owner remove +``` + +## Example + +Imagine you have a package named `hello` and you want to add the principal `2d2zu-vaaaa-aaaak-qb6pq-cai` as an owner. + +mops.toml: +```toml +[package] +name = "hello" +version = "0.1.0" +... +``` + +Add the owner: +``` +mops owner add 2d2zu-vaaaa-aaaak-qb6pq-cai +``` \ No newline at end of file diff --git a/docs/docs/cli/2-publish/05-mops-transfer-ownership.md b/docs/docs/cli/2-publish/05-mops-transfer-ownership.md deleted file mode 100644 index a6db1c8e..00000000 --- a/docs/docs/cli/2-publish/05-mops-transfer-ownership.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -slug: /cli/mops-transfer-ownership -sidebar_label: mops transfer-ownership ---- - -# `mops transfer-ownership` - -Transfer ownership of the current package to another user principal. - -``` -mops transfer-ownership -``` - -:::warning -This action cannot be undone! - -After transfering ownership, you will no longer be able to publish new versions of the package. -::: - -### Example - -Imagine you have a package named `hello` and you want to transfer ownership to the principal `2d2zu-vaaaa-aaaak-qb6pq-cai`. - -mops.toml: -```toml -[package] -name = "hello" -version = "0.1.0" -... -``` - -Transfer ownership: -``` -mops transfer-ownership 2d2zu-vaaaa-aaaak-qb6pq-cai -``` \ No newline at end of file diff --git a/docs/docs/cli/2-publish/06-mops-maintainer.md b/docs/docs/cli/2-publish/06-mops-maintainer.md new file mode 100644 index 00000000..e713d9ed --- /dev/null +++ b/docs/docs/cli/2-publish/06-mops-maintainer.md @@ -0,0 +1,50 @@ +--- +slug: /cli/mops-maintainer +sidebar_label: mops maintainer +--- + +# `mops maintainer` + +List, add or remove maintainers of a package. + +:::info +Check the [Package owners and maintainers](/package-owners-and-maintainers) for more information. +::: + +## `mops maintainer` +Manage maintainers of the current package. + +### `mops maintainer list` + +List all maintainers of the package. + +### `mops maintainer add` + +Add new package maintainer. +``` +mops maintainer add +``` + +### `mops maintainer remove` + +Remove package maintainer. +``` +mops maintainer remove +``` + +## Example + +Imagine you have a package named `hello` and you want to add the principal `2d2zu-vaaaa-aaaak-qb6pq-cai` as a maintainer. + +mops.toml: +```toml +[package] +name = "hello" +version = "0.1.0" +... +``` + +Add the maintainer: +``` +mops maintainer add 2d2zu-vaaaa-aaaak-qb6pq-cai +``` \ No newline at end of file From a7cbbf869b1c4c2b13911b43963cb1e022a0c131 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 25 Nov 2024 12:07:16 +0400 Subject: [PATCH 04/13] [frontend] multiple owners/maintainers --- .../package/PackageRightPanel.svelte | 65 ++++++++-------- frontend/components/package/UserCard.svelte | 54 +++++++++++++ frontend/declarations/main/main.did | 62 +++++++++------ frontend/declarations/main/main.did.d.ts | 62 +++++++++------ frontend/declarations/main/main.did.js | 77 ++++++++++++------- 5 files changed, 215 insertions(+), 105 deletions(-) create mode 100644 frontend/components/package/UserCard.svelte diff --git a/frontend/components/package/PackageRightPanel.svelte b/frontend/components/package/PackageRightPanel.svelte index 694155d2..39ce61d1 100644 --- a/frontend/components/package/PackageRightPanel.svelte +++ b/frontend/components/package/PackageRightPanel.svelte @@ -1,12 +1,11 @@ + +
+
+ {#if user.name} + @{user.name} {user.id} + {:else} + {user.id} + {/if} +
+ {#if user.github} + + GitHub logo + {user.github} + + {/if} + {#if user.twitter} + + + {user.twitter} + + {/if} +
+ + \ No newline at end of file diff --git a/frontend/declarations/main/main.did b/frontend/declarations/main/main.did index c06a6ea8..06a2e107 100644 --- a/frontend/declarations/main/main.did +++ b/frontend/declarations/main/main.did @@ -90,7 +90,7 @@ type Result_6 = variant { err: Err; ok: vec record { - PackageName; + PackageName__1; PackageVersion__1; }; }; @@ -106,18 +106,18 @@ type Result_4 = }; type Result_3 = variant { - err: Err; - ok: FileId; + err: text; + ok; }; type Result_2 = variant { err: Err; - ok: PublishingId; + ok: FileId; }; type Result_1 = variant { - err: text; - ok; + err: Err; + ok: PublishingId; }; type Result = variant { @@ -157,8 +157,10 @@ type PackageSummary__1 = downloadsInLast7Days: nat; downloadsTotal: nat; highestVersion: PackageVersion; + maintainers: vec User; owner: principal; ownerInfo: User; + owners: vec User; publication: PackagePublication; quality: PackageQuality; }; @@ -171,8 +173,10 @@ type PackageSummaryWithChanges__1 = downloadsInLast7Days: nat; downloadsTotal: nat; highestVersion: PackageVersion; + maintainers: vec User; owner: principal; ownerInfo: User; + owners: vec User; publication: PackagePublication; quality: PackageQuality; }; @@ -185,8 +189,10 @@ type PackageSummaryWithChanges = downloadsInLast7Days: nat; downloadsTotal: nat; highestVersion: PackageVersion; + maintainers: vec User; owner: principal; ownerInfo: User; + owners: vec User; publication: PackagePublication; quality: PackageQuality; }; @@ -198,8 +204,10 @@ type PackageSummary = downloadsInLast7Days: nat; downloadsTotal: nat; highestVersion: PackageVersion; + maintainers: vec User; owner: principal; ownerInfo: User; + owners: vec User; publication: PackagePublication; quality: PackageQuality; }; @@ -243,8 +251,10 @@ type PackageDetails = downloadsTotal: nat; fileStats: PackageFileStatsPublic; highestVersion: PackageVersion; + maintainers: vec User; owner: principal; ownerInfo: User; + owners: vec User; publication: PackagePublication; quality: PackageQuality; testStats: TestStats__1; @@ -263,7 +273,7 @@ type PackageConfigV3_Publishing = keywords: vec text; license: text; moc: text; - name: PackageName__1; + name: PackageName; readme: text; repository: text; requirements: opt vec Requirement; @@ -283,7 +293,7 @@ type PackageConfigV3 = keywords: vec text; license: text; moc: text; - name: PackageName__1; + name: PackageName; readme: text; repository: text; requirements: vec Requirement; @@ -301,6 +311,8 @@ type PackageChanges = }; type Main = service { + addMaintainer: (PackageName__1, principal) -> (Result_3); + addOwner: (PackageName__1, principal) -> (Result_3); backup: () -> (); computeHashesForExistingFiles: () -> (); finishPublish: (PublishingId) -> (Result); @@ -308,14 +320,14 @@ type Main = getBackupCanisterId: () -> (principal) query; getDefaultPackages: (text) -> (vec record { - PackageName; + PackageName__1; PackageVersion__1; }) query; getDownloadTrendByPackageId: (PackageId) -> (vec DownloadsSnapshot__1) query; - getDownloadTrendByPackageName: (PackageName) -> + getDownloadTrendByPackageName: (PackageName__1) -> (vec DownloadsSnapshot__1) query; - getFileHashes: (PackageName, PackageVersion__1) -> (Result_8); + getFileHashes: (PackageName__1, PackageVersion__1) -> (Result_8); getFileHashesByPackageIds: (vec PackageId) -> (vec record { PackageId; @@ -324,19 +336,22 @@ type Main = blob; }; }); - getFileHashesQuery: (PackageName, PackageVersion__1) -> (Result_8) query; - getFileIds: (PackageName, PackageVersion__1) -> (Result_7) query; + getFileHashesQuery: (PackageName__1, PackageVersion__1) -> + (Result_8) query; + getFileIds: (PackageName__1, PackageVersion__1) -> (Result_7) query; getHighestSemverBatch: (vec record { - PackageName; + PackageName__1; PackageVersion__1; SemverPart; }) -> (Result_6) query; - getHighestVersion: (PackageName) -> (Result_5) query; + getHighestVersion: (PackageName__1) -> (Result_5) query; getMostDownloadedPackages: () -> (vec PackageSummary) query; getMostDownloadedPackagesIn7Days: () -> (vec PackageSummary) query; getNewPackages: () -> (vec PackageSummary) query; - getPackageDetails: (PackageName, PackageVersion__1) -> (Result_4) query; + getPackageDetails: (PackageName__1, PackageVersion__1) -> (Result_4) query; + getPackageMaintainers: (PackageName__1) -> (vec principal) query; + getPackageOwners: (PackageName__1) -> (vec principal) query; getPackagesByCategory: () -> (vec record { text; vec PackageSummary; @@ -350,17 +365,18 @@ type Main = getTotalPackages: () -> (nat) query; getUser: (principal) -> (opt User__1) query; http_request: (Request) -> (Response) query; - notifyInstall: (PackageName, PackageVersion__1) -> () oneway; + notifyInstall: (PackageName__1, PackageVersion__1) -> () oneway; notifyInstalls: (vec record { - PackageName; + PackageName__1; PackageVersion__1; }) -> () oneway; + removeMaintainer: (PackageName__1, principal) -> (Result_3); + removeOwner: (PackageName__1, principal) -> (Result_3); restore: (nat) -> (); search: (Text, opt nat, opt nat) -> (vec PackageSummary, PageCount) query; - setUserProp: (text, text) -> (Result_1); - startFileUpload: (PublishingId, Text, nat, blob) -> (Result_3); - startPublish: (PackageConfigV3_Publishing) -> (Result_2); - transferOwnership: (PackageName, principal) -> (Result_1); + setUserProp: (text, text) -> (Result_3); + startFileUpload: (PublishingId, Text, nat, blob) -> (Result_2); + startPublish: (PackageConfigV3_Publishing) -> (Result_1); transformRequest: (HttpTransformArg) -> (HttpResponse) query; uploadBenchmarks: (PublishingId, Benchmarks) -> (Result); uploadFileChunk: (PublishingId, FileId, nat, blob) -> (Result); @@ -410,7 +426,7 @@ type DepsStatus = }; type DependencyV2 = record { - name: PackageName__1; + name: PackageName; repo: text; version: text; }; diff --git a/frontend/declarations/main/main.did.d.ts b/frontend/declarations/main/main.did.d.ts index 642779b9..30777e9e 100644 --- a/frontend/declarations/main/main.did.d.ts +++ b/frontend/declarations/main/main.did.d.ts @@ -25,7 +25,7 @@ export interface DepChange { 'newVersion' : string, } export interface DependencyV2 { - 'name' : PackageName__1, + 'name' : PackageName, 'repo' : string, 'version' : string, } @@ -56,6 +56,8 @@ export interface HttpTransformArg { 'response' : HttpResponse, } export interface Main { + 'addMaintainer' : ActorMethod<[PackageName__1, Principal], Result_3>, + 'addOwner' : ActorMethod<[PackageName__1, Principal], Result_3>, 'backup' : ActorMethod<[], undefined>, 'computeHashesForExistingFiles' : ActorMethod<[], undefined>, 'finishPublish' : ActorMethod<[PublishingId], Result>, @@ -63,35 +65,40 @@ export interface Main { 'getBackupCanisterId' : ActorMethod<[], Principal>, 'getDefaultPackages' : ActorMethod< [string], - Array<[PackageName, PackageVersion__1]> + Array<[PackageName__1, PackageVersion__1]> >, 'getDownloadTrendByPackageId' : ActorMethod< [PackageId], Array >, 'getDownloadTrendByPackageName' : ActorMethod< - [PackageName], + [PackageName__1], Array >, - 'getFileHashes' : ActorMethod<[PackageName, PackageVersion__1], Result_8>, + 'getFileHashes' : ActorMethod<[PackageName__1, PackageVersion__1], Result_8>, 'getFileHashesByPackageIds' : ActorMethod< [Array], Array<[PackageId, Array<[FileId, Uint8Array | number[]]>]> >, 'getFileHashesQuery' : ActorMethod< - [PackageName, PackageVersion__1], + [PackageName__1, PackageVersion__1], Result_8 >, - 'getFileIds' : ActorMethod<[PackageName, PackageVersion__1], Result_7>, + 'getFileIds' : ActorMethod<[PackageName__1, PackageVersion__1], Result_7>, 'getHighestSemverBatch' : ActorMethod< - [Array<[PackageName, PackageVersion__1, SemverPart]>], + [Array<[PackageName__1, PackageVersion__1, SemverPart]>], Result_6 >, - 'getHighestVersion' : ActorMethod<[PackageName], Result_5>, + 'getHighestVersion' : ActorMethod<[PackageName__1], Result_5>, 'getMostDownloadedPackages' : ActorMethod<[], Array>, 'getMostDownloadedPackagesIn7Days' : ActorMethod<[], Array>, 'getNewPackages' : ActorMethod<[], Array>, - 'getPackageDetails' : ActorMethod<[PackageName, PackageVersion__1], Result_4>, + 'getPackageDetails' : ActorMethod< + [PackageName__1, PackageVersion__1], + Result_4 + >, + 'getPackageMaintainers' : ActorMethod<[PackageName__1], Array>, + 'getPackageOwners' : ActorMethod<[PackageName__1], Array>, 'getPackagesByCategory' : ActorMethod< [], Array<[string, Array]> @@ -105,23 +112,24 @@ export interface Main { 'getTotalPackages' : ActorMethod<[], bigint>, 'getUser' : ActorMethod<[Principal], [] | [User__1]>, 'http_request' : ActorMethod<[Request], Response>, - 'notifyInstall' : ActorMethod<[PackageName, PackageVersion__1], undefined>, + 'notifyInstall' : ActorMethod<[PackageName__1, PackageVersion__1], undefined>, 'notifyInstalls' : ActorMethod< - [Array<[PackageName, PackageVersion__1]>], + [Array<[PackageName__1, PackageVersion__1]>], undefined >, + 'removeMaintainer' : ActorMethod<[PackageName__1, Principal], Result_3>, + 'removeOwner' : ActorMethod<[PackageName__1, Principal], Result_3>, 'restore' : ActorMethod<[bigint], undefined>, 'search' : ActorMethod< [Text, [] | [bigint], [] | [bigint]], [Array, PageCount] >, - 'setUserProp' : ActorMethod<[string, string], Result_1>, + 'setUserProp' : ActorMethod<[string, string], Result_3>, 'startFileUpload' : ActorMethod< [PublishingId, Text, bigint, Uint8Array | number[]], - Result_3 + Result_2 >, - 'startPublish' : ActorMethod<[PackageConfigV3_Publishing], Result_2>, - 'transferOwnership' : ActorMethod<[PackageName, Principal], Result_1>, + 'startPublish' : ActorMethod<[PackageConfigV3_Publishing], Result_1>, 'transformRequest' : ActorMethod<[HttpTransformArg], HttpResponse>, 'uploadBenchmarks' : ActorMethod<[PublishingId, Benchmarks], Result>, 'uploadFileChunk' : ActorMethod< @@ -145,7 +153,7 @@ export interface PackageConfigV3 { 'scripts' : Array -
+
{#if user.name} - @{user.name} {user.id} + {user.name} {user.id} {:else} {user.id} {/if}
- {#if user.github} - - GitHub logo - {user.github} - - {/if} - {#if user.twitter} - - - {user.twitter} - + {#if !compact} + {#if user.github} + + GitHub logo + {user.github} + + {/if} + {#if user.twitter} + + + {user.twitter} + + {/if} {/if}
@@ -49,7 +52,15 @@ filter: hue-rotate(45deg) contrast(0.6); } - .user-card { + .user-card.compact .principal-text::before { + content: '('; + } + + .user-card.compact .principal-text::after { + content: ')'; + } + + .user-card:not(.compact) { display: flex; flex-direction: column; gap: 6px; diff --git a/frontend/declarations/main/main.did b/frontend/declarations/main/main.did index 06a2e107..5d82914f 100644 --- a/frontend/declarations/main/main.did +++ b/frontend/declarations/main/main.did @@ -162,6 +162,7 @@ type PackageSummary__1 = ownerInfo: User; owners: vec User; publication: PackagePublication; + publisher: User; quality: PackageQuality; }; type PackageSummaryWithChanges__1 = @@ -178,6 +179,7 @@ type PackageSummaryWithChanges__1 = ownerInfo: User; owners: vec User; publication: PackagePublication; + publisher: User; quality: PackageQuality; }; type PackageSummaryWithChanges = @@ -194,6 +196,7 @@ type PackageSummaryWithChanges = ownerInfo: User; owners: vec User; publication: PackagePublication; + publisher: User; quality: PackageQuality; }; type PackageSummary = @@ -209,6 +212,7 @@ type PackageSummary = ownerInfo: User; owners: vec User; publication: PackagePublication; + publisher: User; quality: PackageQuality; }; type PackageQuality = @@ -256,6 +260,7 @@ type PackageDetails = ownerInfo: User; owners: vec User; publication: PackagePublication; + publisher: User; quality: PackageQuality; testStats: TestStats__1; versionHistory: vec PackageSummaryWithChanges__1; diff --git a/frontend/declarations/main/main.did.d.ts b/frontend/declarations/main/main.did.d.ts index 30777e9e..79d52110 100644 --- a/frontend/declarations/main/main.did.d.ts +++ b/frontend/declarations/main/main.did.d.ts @@ -194,6 +194,7 @@ export interface PackageDetails { 'depAlias' : string, 'deps' : Array, 'quality' : PackageQuality, + 'publisher' : User, 'testStats' : TestStats__1, 'highestVersion' : PackageVersion, 'downloadsTotal' : bigint, @@ -237,6 +238,7 @@ export interface PackageSummary { 'owner' : Principal, 'depAlias' : string, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : bigint, 'downloadsInLast30Days' : bigint, @@ -251,6 +253,7 @@ export interface PackageSummaryWithChanges { 'owner' : Principal, 'depAlias' : string, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : bigint, 'downloadsInLast30Days' : bigint, @@ -266,6 +269,7 @@ export interface PackageSummaryWithChanges__1 { 'owner' : Principal, 'depAlias' : string, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : bigint, 'downloadsInLast30Days' : bigint, @@ -281,6 +285,7 @@ export interface PackageSummary__1 { 'owner' : Principal, 'depAlias' : string, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : bigint, 'downloadsInLast30Days' : bigint, diff --git a/frontend/declarations/main/main.did.js b/frontend/declarations/main/main.did.js index e7089746..7d84cffa 100644 --- a/frontend/declarations/main/main.did.js +++ b/frontend/declarations/main/main.did.js @@ -96,6 +96,7 @@ export const idlFactory = ({ IDL }) => { 'owner' : IDL.Principal, 'depAlias' : IDL.Text, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : IDL.Nat, 'downloadsInLast30Days' : IDL.Nat, @@ -126,6 +127,7 @@ export const idlFactory = ({ IDL }) => { 'owner' : IDL.Principal, 'depAlias' : IDL.Text, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : IDL.Nat, 'downloadsInLast30Days' : IDL.Nat, @@ -170,6 +172,7 @@ export const idlFactory = ({ IDL }) => { 'owner' : IDL.Principal, 'depAlias' : IDL.Text, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : IDL.Nat, 'downloadsInLast30Days' : IDL.Nat, @@ -187,6 +190,7 @@ export const idlFactory = ({ IDL }) => { 'depAlias' : IDL.Text, 'deps' : IDL.Vec(PackageSummary__1), 'quality' : PackageQuality, + 'publisher' : User, 'testStats' : TestStats__1, 'highestVersion' : PackageVersion, 'downloadsTotal' : IDL.Nat, @@ -209,6 +213,7 @@ export const idlFactory = ({ IDL }) => { 'owner' : IDL.Principal, 'depAlias' : IDL.Text, 'quality' : PackageQuality, + 'publisher' : User, 'highestVersion' : PackageVersion, 'downloadsTotal' : IDL.Nat, 'downloadsInLast30Days' : IDL.Nat, From e62b7bfba0f63bcc81546b01a6bca08f6fe5e5c8 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 25 Nov 2024 13:32:30 +0400 Subject: [PATCH 13/13] [cli] update changelog --- cli/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index fc60ecab..9df5b4b2 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # Mops CLI Changelog +## Unreleased +- Removed `mops transfer-ownership` command +- Added `mops owner` command to manage package owners ([docs](https://docs.mops.one/cli/mops-owner)) +- Added `mops maintainers` command to manage package maintainers ([docs](https://docs.mops.one/cli/mops-maintainers)) +- Fixed bug where `mops watch` would fail if dfx.json did not exist + ## 1.1.1 - `moc-wrapper` now adds hostname to the moc path cache(`.mops/moc-*` filename) to avoid errors when running in Dev Containers - `mops watch` now deploys canisters with the `--yes` flag to skip data loss confirmation