Merge packages: roles, Actions access, source label, soft delete, API
| 18 | 18 | mod notifications; | |
| 19 | 19 | mod oauth; | |
| 20 | 20 | mod oidc; | |
| 21 | + | mod packages; | |
| 21 | 22 | mod openapi; | |
| 22 | 23 | mod pins; | |
| 23 | 24 | mod projects; |
| 11 | 11 | use crate::artifacts::ArtifactsOp; | |
| 12 | 12 | use crate::deploy_keys::DeployKeysOp; | |
| 13 | 13 | use crate::deployments::DeploymentsOp; | |
| 14 | + | use crate::packages::PackagesOp; | |
| 14 | 15 | use crate::protection::ProtectionOp; | |
| 15 | 16 | use crate::token_policy::TokenOp; | |
| 16 | 17 | use crate::operations::Op; | |
| 401 | 402 | ], | |
| 402 | 403 | ), | |
| 403 | 404 | ( | |
| 405 | + | "Packages", | |
| 406 | + | "A workspace's packages in every registry: their versions and download counts, deleting and restoring them within 30 days, their visibility and repository, the people and teams with a role on them, and which repositories' workflows may use them (Manage Actions access). A package is named by its type and its name, URL-encoded where it holds a slash.", | |
| 407 | + | &[ | |
| 408 | + | Op::Packages(PackagesOp::ListPackages), | |
| 409 | + | Op::Packages(PackagesOp::GetPackage), | |
| 410 | + | Op::Packages(PackagesOp::UpdatePackage), | |
| 411 | + | Op::Packages(PackagesOp::DeletePackage), | |
| 412 | + | Op::Packages(PackagesOp::RestorePackage), | |
| 413 | + | Op::Packages(PackagesOp::ListVersions), | |
| 414 | + | Op::Packages(PackagesOp::GetVersion), | |
| 415 | + | Op::Packages(PackagesOp::DeleteVersion), | |
| 416 | + | Op::Packages(PackagesOp::RestoreVersion), | |
| 417 | + | Op::Packages(PackagesOp::LinkPackage), | |
| 418 | + | Op::Packages(PackagesOp::UnlinkPackage), | |
| 419 | + | Op::Packages(PackagesOp::ListAccess), | |
| 420 | + | Op::Packages(PackagesOp::SetAccess), | |
| 421 | + | Op::Packages(PackagesOp::RemoveAccess), | |
| 422 | + | Op::Packages(PackagesOp::ListActionsAccess), | |
| 423 | + | Op::Packages(PackagesOp::SetActionsAccess), | |
| 424 | + | Op::Packages(PackagesOp::RemoveActionsAccess), | |
| 425 | + | ], | |
| 426 | + | ), | |
| 427 | + | ( | |
| 404 | 428 | "Deployments", | |
| 405 | 429 | "A repository's deployments wherever they run: reported from any CI with these routes, made by g1t Actions jobs with an `environment:`, or built on g1t.page. Each has statuses, shows on its commit as the check `deploy / <environment>`, and belongs to an environment.", | |
| 406 | 430 | &[ | |
| 692 | 716 | Op::Tokens(op) => op.title(), | |
| 693 | 717 | Op::Artifacts(op) => op.title(), | |
| 694 | 718 | Op::DeployKeys(op) => op.title(), | |
| 719 | + | Op::Packages(op) => op.title(), | |
| 695 | 720 | } | |
| 696 | 721 | } | |
| 697 | 722 |
| 31 | 31 | use crate::artifacts::ArtifactsOp; | |
| 32 | 32 | use crate::deploy_keys::DeployKeysOp; | |
| 33 | 33 | use crate::deployments::DeploymentsOp; | |
| 34 | + | use crate::packages::PackagesOp; | |
| 34 | 35 | use crate::protection::ProtectionOp; | |
| 35 | 36 | use crate::token_policy::TokenOp; | |
| 36 | 37 | use crate::rules::RulesOp; | |
| 64 | 65 | pub projects: Fetcher, | |
| 65 | 66 | /// Deployments wherever they run, and environments. | |
| 66 | 67 | pub deployments: Fetcher, | |
| 68 | + | /// Packages: their settings, versions, deleting and restoring them. | |
| 69 | + | pub packages: Fetcher, | |
| 67 | 70 | /// Where the request came in, for its audit entries. | |
| 68 | 71 | pub audit: crate::audit::AuditContext, | |
| 69 | 72 | /// Set for a request made with an agent's token: all it may do. | |
| 89 | 92 | security: env.service("SECURITY")?, | |
| 90 | 93 | projects: env.service("PROJECTS")?, | |
| 91 | 94 | deployments: env.service("DEPLOYMENTS")?, | |
| 95 | + | packages: env.service("PACKAGES")?, | |
| 92 | 96 | scope: None, | |
| 93 | 97 | audit: crate::audit::AuditContext::default(), | |
| 94 | 98 | addresses: crate::addresses::Addresses::from_env(env), | |
| 302 | 306 | Artifacts(ArtifactsOp), | |
| 303 | 307 | /// A repository's deploy keys: deploy_keys.rs. | |
| 304 | 308 | DeployKeys(DeployKeysOp), | |
| 309 | + | /// A workspace's packages, their versions, deleting and restoring | |
| 310 | + | /// them, and who may use them: packages.rs. | |
| 311 | + | Packages(PackagesOp), | |
| 305 | 312 | } | |
| 306 | 313 | ||
| 307 | 314 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| 664 | 671 | } | |
| 665 | 672 | ||
| 666 | 673 | impl Op { | |
| 667 | − | pub const ALL: [Op; 291] = [ | |
| 674 | + | pub const ALL: [Op; 308] = [ | |
| 668 | 675 | Op::Whoami, | |
| 669 | 676 | Op::GetWorkspace, | |
| 670 | 677 | Op::CreateWorkspace, | |
| 956 | 963 | Op::Tokens(TokenOp::ListTokenRequests), | |
| 957 | 964 | Op::Tokens(TokenOp::ReviewTokenRequest), | |
| 958 | 965 | Op::Tokens(TokenOp::RevokeMemberToken), | |
| 966 | + | Op::Packages(PackagesOp::ListPackages), | |
| 967 | + | Op::Packages(PackagesOp::GetPackage), | |
| 968 | + | Op::Packages(PackagesOp::ListVersions), | |
| 969 | + | Op::Packages(PackagesOp::GetVersion), | |
| 970 | + | Op::Packages(PackagesOp::ListAccess), | |
| 971 | + | Op::Packages(PackagesOp::ListActionsAccess), | |
| 972 | + | Op::Packages(PackagesOp::UpdatePackage), | |
| 973 | + | Op::Packages(PackagesOp::LinkPackage), | |
| 974 | + | Op::Packages(PackagesOp::UnlinkPackage), | |
| 975 | + | Op::Packages(PackagesOp::SetAccess), | |
| 976 | + | Op::Packages(PackagesOp::RemoveAccess), | |
| 977 | + | Op::Packages(PackagesOp::SetActionsAccess), | |
| 978 | + | Op::Packages(PackagesOp::RemoveActionsAccess), | |
| 979 | + | Op::Packages(PackagesOp::DeletePackage), | |
| 980 | + | Op::Packages(PackagesOp::RestorePackage), | |
| 981 | + | Op::Packages(PackagesOp::DeleteVersion), | |
| 982 | + | Op::Packages(PackagesOp::RestoreVersion), | |
| 959 | 983 | ]; | |
| 960 | 984 | ||
| 961 | 985 | pub fn by_name(name: &str) -> Option<Op> { | |
| 1158 | 1182 | Op::Tokens(op) => op.name(), | |
| 1159 | 1183 | Op::Artifacts(op) => op.name(), | |
| 1160 | 1184 | Op::DeployKeys(op) => op.name(), | |
| 1185 | + | Op::Packages(op) => op.name(), | |
| 1161 | 1186 | } | |
| 1162 | 1187 | } | |
| 1163 | 1188 | ||
| 1690 | 1715 | Op::Tokens(op) => op.description(), | |
| 1691 | 1716 | Op::Artifacts(op) => op.description(), | |
| 1692 | 1717 | Op::DeployKeys(op) => op.description(), | |
| 1718 | + | Op::Packages(op) => op.description(), | |
| 1693 | 1719 | } | |
| 1694 | 1720 | } | |
| 1695 | 1721 | ||
| 3114 | 3140 | Op::Tokens(op) => op.input(), | |
| 3115 | 3141 | Op::Artifacts(op) => op.input(), | |
| 3116 | 3142 | Op::DeployKeys(op) => op.input(), | |
| 3143 | + | Op::Packages(op) => op.input(), | |
| 3117 | 3144 | } | |
| 3118 | 3145 | } | |
| 3119 | 3146 | ||
| 3130 | 3157 | if let Op::Artifacts(op) = self { | |
| 3131 | 3158 | return op.writes(); | |
| 3132 | 3159 | } | |
| 3160 | + | // So are public packages. | |
| 3161 | + | if let Op::Packages(op) = self { | |
| 3162 | + | return !op.anonymous(); | |
| 3163 | + | } | |
| 3133 | 3164 | !matches!( | |
| 3134 | 3165 | self, | |
| 3135 | 3166 | Op::ListRepos | |
| 3176 | 3207 | if let Op::Rules(op) = self { | |
| 3177 | 3208 | return op.needs_repo(); | |
| 3178 | 3209 | } | |
| 3210 | + | // A package belongs to its workspace; its repository is in `repo` | |
| 3211 | + | // only for Manage Actions access, checked by the packages service. | |
| 3212 | + | if let Op::Packages(_) = self { | |
| 3213 | + | return false; | |
| 3214 | + | } | |
| 3179 | 3215 | if let Op::About(op) = self { | |
| 3180 | 3216 | return op.needs_repo(); | |
| 3181 | 3217 | } | |
| 5322 | 5358 | Op::Tokens(op) => crate::token_policy::run(op, services, viewer, input).await, | |
| 5323 | 5359 | Op::Artifacts(op) => crate::artifacts::run(op, services, viewer, input).await, | |
| 5324 | 5360 | Op::DeployKeys(op) => crate::deploy_keys::run(op, services, viewer, input).await, | |
| 5361 | + | Op::Packages(op) => crate::packages::run(op, services, viewer, input).await, | |
| 5325 | 5362 | Op::ReopenSecurityAlert => { | |
| 5326 | 5363 | let changed: Outcome<AlertChange> = call( | |
| 5327 | 5364 | &services.security, |
| 1 | + | //! Packages over REST and MCP, at GitHub's addresses with the workspace in | |
| 2 | + | //! place of the organization: a workspace's packages and their versions, | |
| 3 | + | //! deleting and restoring them, their visibility and repository, who has a | |
| 4 | + | //! role on them, and which repositories' workflows may use them (Manage | |
| 5 | + | //! Actions access). | |
| 6 | + | //! | |
| 7 | + | //! The packages service decides who may do what (`g1t_contracts::packages`) | |
| 8 | + | //! and records the audit entries; this is its public shape, in snake_case. | |
| 9 | + | //! A package is named by its type (`container`, `npm`, `cargo`, `maven`, | |
| 10 | + | //! `nuget`, `rubygems`, `composer`) and its name, URL-encoded when it holds | |
| 11 | + | //! a slash (`web%2Fworker`). | |
| 12 | + | ||
| 13 | + | use g1t_contracts::packages::*; | |
| 14 | + | use g1t_contracts::{FailureCode, Outcome, User, Viewer}; | |
| 15 | + | use serde::de::DeserializeOwned; | |
| 16 | + | use serde::Serialize; | |
| 17 | + | use serde_json::{Value, json}; | |
| 18 | + | use worker::Result; | |
| 19 | + | ||
| 20 | + | use crate::operations::Services; | |
| 21 | + | ||
| 22 | + | /// One operation on packages. | |
| 23 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 24 | + | pub enum PackagesOp { | |
| 25 | + | ListPackages, | |
| 26 | + | GetPackage, | |
| 27 | + | ListVersions, | |
| 28 | + | GetVersion, | |
| 29 | + | ListAccess, | |
| 30 | + | ListActionsAccess, | |
| 31 | + | UpdatePackage, | |
| 32 | + | LinkPackage, | |
| 33 | + | UnlinkPackage, | |
| 34 | + | SetAccess, | |
| 35 | + | RemoveAccess, | |
| 36 | + | SetActionsAccess, | |
| 37 | + | RemoveActionsAccess, | |
| 38 | + | DeletePackage, | |
| 39 | + | RestorePackage, | |
| 40 | + | DeleteVersion, | |
| 41 | + | RestoreVersion, | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | impl PackagesOp { | |
| 45 | + | /// Every one: `Op::ALL` lists each as `Op::Packages(…)`, which a test | |
| 46 | + | /// checks against this. | |
| 47 | + | #[cfg(test)] | |
| 48 | + | pub const ALL: [PackagesOp; 17] = [ | |
| 49 | + | PackagesOp::ListPackages, | |
| 50 | + | PackagesOp::GetPackage, | |
| 51 | + | PackagesOp::ListVersions, | |
| 52 | + | PackagesOp::GetVersion, | |
| 53 | + | PackagesOp::ListAccess, | |
| 54 | + | PackagesOp::ListActionsAccess, | |
| 55 | + | PackagesOp::UpdatePackage, | |
| 56 | + | PackagesOp::LinkPackage, | |
| 57 | + | PackagesOp::UnlinkPackage, | |
| 58 | + | PackagesOp::SetAccess, | |
| 59 | + | PackagesOp::RemoveAccess, | |
| 60 | + | PackagesOp::SetActionsAccess, | |
| 61 | + | PackagesOp::RemoveActionsAccess, | |
| 62 | + | PackagesOp::DeletePackage, | |
| 63 | + | PackagesOp::RestorePackage, | |
| 64 | + | PackagesOp::DeleteVersion, | |
| 65 | + | PackagesOp::RestoreVersion, | |
| 66 | + | ]; | |
| 67 | + | ||
| 68 | + | pub fn name(self) -> &'static str { | |
| 69 | + | match self { | |
| 70 | + | PackagesOp::ListPackages => "list_packages", | |
| 71 | + | PackagesOp::GetPackage => "get_package", | |
| 72 | + | PackagesOp::ListVersions => "list_package_versions", | |
| 73 | + | PackagesOp::GetVersion => "get_package_version", | |
| 74 | + | PackagesOp::ListAccess => "list_package_access", | |
| 75 | + | PackagesOp::ListActionsAccess => "list_package_actions_access", | |
| 76 | + | PackagesOp::UpdatePackage => "update_package", | |
| 77 | + | PackagesOp::LinkPackage => "link_package", | |
| 78 | + | PackagesOp::UnlinkPackage => "unlink_package", | |
| 79 | + | PackagesOp::SetAccess => "set_package_access", | |
| 80 | + | PackagesOp::RemoveAccess => "remove_package_access", | |
| 81 | + | PackagesOp::SetActionsAccess => "set_package_actions_access", | |
| 82 | + | PackagesOp::RemoveActionsAccess => "remove_package_actions_access", | |
| 83 | + | PackagesOp::DeletePackage => "delete_package", | |
| 84 | + | PackagesOp::RestorePackage => "restore_package", | |
| 85 | + | PackagesOp::DeleteVersion => "delete_package_version", | |
| 86 | + | PackagesOp::RestoreVersion => "restore_package_version", | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | /// For the API reference. | |
| 91 | + | pub fn title(self) -> &'static str { | |
| 92 | + | match self { | |
| 93 | + | PackagesOp::ListPackages => "List a workspace's packages", | |
| 94 | + | PackagesOp::GetPackage => "Get a package", | |
| 95 | + | PackagesOp::ListVersions => "List a package's versions", | |
| 96 | + | PackagesOp::GetVersion => "Get a package version", | |
| 97 | + | PackagesOp::ListAccess => "List who has access to a package", | |
| 98 | + | PackagesOp::ListActionsAccess => "List a package's Actions access", | |
| 99 | + | PackagesOp::UpdatePackage => "Update a package", | |
| 100 | + | PackagesOp::LinkPackage => "Link a package to a repository", | |
| 101 | + | PackagesOp::UnlinkPackage => "Unlink a package from its repository", | |
| 102 | + | PackagesOp::SetAccess => "Give a person or team a role on a package", | |
| 103 | + | PackagesOp::RemoveAccess => "Remove a person's or team's role on a package", | |
| 104 | + | PackagesOp::SetActionsAccess => "Give a repository's workflows access to a package", | |
| 105 | + | PackagesOp::RemoveActionsAccess => "Remove a repository's Actions access to a package", | |
| 106 | + | PackagesOp::DeletePackage => "Delete a package", | |
| 107 | + | PackagesOp::RestorePackage => "Restore a package", | |
| 108 | + | PackagesOp::DeleteVersion => "Delete a package version", | |
| 109 | + | PackagesOp::RestoreVersion => "Restore a package version", | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | pub fn description(self) -> &'static str { | |
| 114 | + | match self { | |
| 115 | + | PackagesOp::ListPackages => "List a workspace's packages you may pull, most recently updated first: each with its id, name, package_type, address (what a client is given, such as g1t.sh/acme/web), visibility, the repository it is linked to, version_count, latest, size_in_bytes, download_count, inherit_access and html_url. Narrow with package_type and q (part of the name). With state deleted, its deleted packages that can still be restored instead, those you administer, each with deleted_at, deleted_by and purge_at. Public packages are open to anyone.", | |
| 116 | + | PackagesOp::GetPackage => "Get one package by its package_type and package_name (URL-encode a slash in a REST path: web%2Fworker). Not found when you may not pull it, as for one that does not exist.", | |
| 117 | + | PackagesOp::ListVersions => "List a package's versions, newest first: each with its id (ver_…), name (the version, or for a container image its digest), digest, size_in_bytes, download_count, tags, media_type, platforms, published_by and created_at. With state deleted, its deleted versions that can still be restored, with deleted_at, deleted_by and purge_at: for the package's admins only.", | |
| 118 | + | PackagesOp::GetVersion => "Get one version of a package by its id (ver_…), its version, its digest, or a tag that points to it.", | |
| 119 | + | PackagesOp::ListAccess => "Who has a role on a package itself (read pulls, write publishes, admin deletes, restores and changes its settings), people and teams, with inherit_access: whether a linked package also takes its repository's roles. Owners of the workspace administer every package. For the package's admins.", | |
| 120 | + | PackagesOp::ListActionsAccess => "Which repositories' workflows may use a package with their job token (G1T_TOKEN), with the read or write role: its linked repository (linked, always write) and those added under Manage Actions access. A job's token from any other repository is refused. For the package's admins.", | |
| 121 | + | PackagesOp::UpdatePackage => "Change a package's visibility (public or private: an unlinked package only, as a linked one has its repository's) or, for a linked package, inherit_access: whether it takes its repository's roles. Off, only the roles given on the package itself and the workspace's owners count. Takes the Admin role on the package. Returns the package.", | |
| 122 | + | PackagesOp::LinkPackage => "Link a package to a repository of its workspace (repository: its name or owner/name): it then has that repository's visibility and, unless inherit_access is off, its roles, and the repository's workflows may publish it. Takes the Admin role on the package and on the repository. Returns the package.", | |
| 123 | + | PackagesOp::UnlinkPackage => "Unlink a package from its repository: it is then the workspace's, private until someone makes it public, and the repository's workflows lose their access unless it is added under Manage Actions access. Takes the Admin role on the package. Returns the package.", | |
| 124 | + | PackagesOp::SetAccess => "Give a person (username) or a team of the workspace (team: its slug) the read, write or admin role on a package, or change theirs. It adds to what its repository or workspace gives them. Takes the Admin role on the package. Returns everyone with a role on it.", | |
| 125 | + | PackagesOp::RemoveAccess => "Take a person's (username) or team's (team) role on a package away. What its repository or workspace gives them stays. Takes the Admin role on the package. Returns everyone left with a role on it.", | |
| 126 | + | PackagesOp::SetActionsAccess => "Let a repository of the package's workspace (repository: its name or owner/name) use the package from its workflows, with the read or write role, or change its role. Takes the Admin role on the package. Returns the package's Actions access.", | |
| 127 | + | PackagesOp::RemoveActionsAccess => "Stop a repository's workflows using a package. The linked repository's access cannot be removed: unlink the package instead. Takes the Admin role on the package. Returns the package's Actions access.", | |
| 128 | + | PackagesOp::DeletePackage => "Delete a package and every version: it is gone from the registries at once, and can be restored for 30 days, during which its name cannot be taken. Takes the Admin role on the package.", | |
| 129 | + | PackagesOp::RestorePackage => "Restore a deleted package, with the versions it had, while it can be (30 days after it was deleted). Takes the Admin role on it. Returns the package.", | |
| 130 | + | PackagesOp::DeleteVersion => "Delete one version by its id, version, digest or a tag that points to it: it is gone from the registries at once, with its tags, and can be restored for 30 days. Its version (or digest) cannot be published again until then. Composer versions follow their repository's tags: delete the tag instead. Takes the Admin role on the package.", | |
| 131 | + | PackagesOp::RestoreVersion => "Restore a deleted version by its id or version, while it can be (30 days after it was deleted), with the tags that still pointed to it. Takes the Admin role on the package. Returns the version.", | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | /// Whether it changes anything. | |
| 136 | + | #[cfg(test)] | |
| 137 | + | pub fn writes(self) -> bool { | |
| 138 | + | !matches!( | |
| 139 | + | self, | |
| 140 | + | PackagesOp::ListPackages | |
| 141 | + | | PackagesOp::GetPackage | |
| 142 | + | | PackagesOp::ListVersions | |
| 143 | + | | PackagesOp::GetVersion | |
| 144 | + | | PackagesOp::ListAccess | |
| 145 | + | | PackagesOp::ListActionsAccess | |
| 146 | + | ) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | /// Whether anyone may call it, signed in or not: reading public packages. | |
| 150 | + | pub fn anonymous(self) -> bool { | |
| 151 | + | matches!(self, PackagesOp::ListPackages | PackagesOp::GetPackage | PackagesOp::ListVersions | PackagesOp::GetVersion) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | pub fn input(self) -> Value { | |
| 155 | + | let workspace = json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." }); | |
| 156 | + | let package_type = json!({ | |
| 157 | + | "type": "string", | |
| 158 | + | "enum": ["container", "npm", "cargo", "maven", "nuget", "rubygems", "composer"], | |
| 159 | + | "description": "The registry: container (also docker), npm, cargo, maven, nuget, rubygems or composer.", | |
| 160 | + | }); | |
| 161 | + | let package_name = json!({ "type": "string", "description": "The package's name without the workspace: web for g1t.sh/acme/web, web/worker for an image with more parts, group:artifact for Maven." }); | |
| 162 | + | let version_id = json!({ "type": "string", "description": "The version's id (ver_…), its version, its digest, or a tag that points to it." }); | |
| 163 | + | let role = |roles: &[&str]| json!({ "type": "string", "enum": roles, "description": "read pulls, write publishes, admin deletes, restores and changes its settings." }); | |
| 164 | + | let package = |mut properties: Value| { | |
| 165 | + | properties["workspace"] = workspace.clone(); | |
| 166 | + | properties["package_type"] = package_type.clone(); | |
| 167 | + | properties["package_name"] = package_name.clone(); | |
| 168 | + | properties | |
| 169 | + | }; | |
| 170 | + | let base = ["workspace", "package_type", "package_name"]; | |
| 171 | + | let (properties, required): (Value, Vec<&str>) = match self { | |
| 172 | + | PackagesOp::ListPackages => ( | |
| 173 | + | json!({ | |
| 174 | + | "workspace": workspace, | |
| 175 | + | "package_type": package_type, | |
| 176 | + | "q": { "type": "string", "description": "Only packages whose name holds this." }, | |
| 177 | + | "state": { "type": "string", "enum": ["active", "deleted"], "description": "active (the default), or deleted: deleted packages that can still be restored." }, | |
| 178 | + | }), | |
| 179 | + | vec!["workspace"], | |
| 180 | + | ), | |
| 181 | + | PackagesOp::GetPackage | |
| 182 | + | | PackagesOp::ListAccess | |
| 183 | + | | PackagesOp::ListActionsAccess | |
| 184 | + | | PackagesOp::UnlinkPackage | |
| 185 | + | | PackagesOp::DeletePackage | |
| 186 | + | | PackagesOp::RestorePackage => (package(json!({})), base.to_vec()), | |
| 187 | + | PackagesOp::ListVersions => ( | |
| 188 | + | package(json!({ | |
| 189 | + | "state": { "type": "string", "enum": ["active", "deleted"], "description": "active (the default), or deleted: deleted versions that can still be restored." }, | |
| 190 | + | })), | |
| 191 | + | base.to_vec(), | |
| 192 | + | ), | |
| 193 | + | PackagesOp::GetVersion | PackagesOp::DeleteVersion | PackagesOp::RestoreVersion => { | |
| 194 | + | (package(json!({ "version_id": version_id })), [base.as_slice(), &["version_id"]].concat()) | |
| 195 | + | } | |
| 196 | + | PackagesOp::UpdatePackage => ( | |
| 197 | + | package(json!({ | |
| 198 | + | "visibility": { "type": "string", "enum": ["public", "private"], "description": "Who may pull an unlinked package: anyone, or the workspace's members by its base permission." }, | |
| 199 | + | "inherit_access": { "type": "boolean", "description": "For a linked package: whether it takes its repository's roles." }, | |
| 200 | + | })), | |
| 201 | + | base.to_vec(), | |
| 202 | + | ), | |
| 203 | + | PackagesOp::LinkPackage => ( | |
| 204 | + | package(json!({ "repository": { "type": "string", "description": "A repository of the package's workspace: its name, or owner/name." } })), | |
| 205 | + | [base.as_slice(), &["repository"]].concat(), | |
| 206 | + | ), | |
| 207 | + | PackagesOp::SetAccess => ( | |
| 208 | + | package(json!({ | |
| 209 | + | "username": { "type": "string", "description": "The person's username. Give this or team." }, | |
| 210 | + | "team": { "type": "string", "description": "A team of the workspace: its slug, or workspace/slug. Give this or username." }, | |
| 211 | + | "role": role(&["read", "write", "admin"]), | |
| 212 | + | })), | |
| 213 | + | [base.as_slice(), &["role"]].concat(), | |
| 214 | + | ), | |
| 215 | + | PackagesOp::RemoveAccess => ( | |
| 216 | + | package(json!({ | |
| 217 | + | "username": { "type": "string", "description": "The person's username. Give this or team." }, | |
| 218 | + | "team": { "type": "string", "description": "The team's slug, or workspace/slug. Give this or username." }, | |
| 219 | + | })), | |
| 220 | + | base.to_vec(), | |
| 221 | + | ), | |
| 222 | + | PackagesOp::SetActionsAccess => ( | |
| 223 | + | package(json!({ | |
| 224 | + | "repository": { "type": "string", "description": "A repository of the package's workspace: its name, or owner/name." }, | |
| 225 | + | "role": json!({ "type": "string", "enum": ["read", "write"], "description": "read pulls the package from the repository's workflows; write publishes it too." }), | |
| 226 | + | })), | |
| 227 | + | [base.as_slice(), &["repository", "role"]].concat(), | |
| 228 | + | ), | |
| 229 | + | PackagesOp::RemoveActionsAccess => ( | |
| 230 | + | package(json!({ "repository": { "type": "string", "description": "The repository: its name, or owner/name." } })), | |
| 231 | + | [base.as_slice(), &["repository"]].concat(), | |
| 232 | + | ), | |
| 233 | + | }; | |
| 234 | + | json!({ "type": "object", "properties": properties, "required": required }) | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | fn text(input: &Value, key: &str) -> Option<String> { | |
| 239 | + | input[key].as_str().map(str::trim).filter(|t| !t.is_empty()).map(str::to_owned) | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | /// The registry `package_type` names: GitHub's `docker` is a container | |
| 243 | + | /// image too. | |
| 244 | + | pub(crate) fn ecosystem(text: &str) -> Option<Ecosystem> { | |
| 245 | + | match text.trim().to_ascii_lowercase().as_str() { | |
| 246 | + | "docker" | "container" | "oci" => Some(Ecosystem::Container), | |
| 247 | + | other => Ecosystem::parse(other).filter(|ecosystem| *ecosystem != Ecosystem::Go), | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | /// The page of a package on the site. | |
| 252 | + | fn html_url(site: &str, package: &PackageSummary) -> String { | |
| 253 | + | format!( | |
| 254 | + | "{}/{}/-/packages/{}/{}", | |
| 255 | + | site.trim_end_matches('/'), | |
| 256 | + | package.workspace, | |
| 257 | + | package.ecosystem.as_str(), | |
| 258 | + | package.name | |
| 259 | + | ) | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | /// A package as the API shows one. | |
| 263 | + | pub fn package_json(package: &PackageSummary, site: &str) -> Value { | |
| 264 | + | json!({ | |
| 265 | + | "id": package.id, | |
| 266 | + | "name": package.name, | |
| 267 | + | "package_type": package.ecosystem.as_str(), | |
| 268 | + | "workspace": package.workspace, | |
| 269 | + | "address": package.address, | |
| 270 | + | "visibility": package.visibility.as_str(), | |
| 271 | + | "repository": package.repo.as_ref().map(|repo| json!({ | |
| 272 | + | "id": repo.id, | |
| 273 | + | "name": repo.name, | |
| 274 | + | "full_name": format!("{}/{}", repo.namespace, repo.name), | |
| 275 | + | })), | |
| 276 | + | "description": package.description, | |
| 277 | + | "version_count": package.versions, | |
| 278 | + | "latest": package.latest, | |
| 279 | + | "size_in_bytes": package.size, | |
| 280 | + | "download_count": package.downloads, | |
| 281 | + | "inherit_access": package.inherit_access, | |
| 282 | + | "created_at": package.created_at, | |
| 283 | + | "updated_at": package.updated_at, | |
| 284 | + | "deleted_at": package.deleted_at, | |
| 285 | + | "deleted_by": package.deleted_by, | |
| 286 | + | "purge_at": package.purge_at, | |
| 287 | + | "html_url": html_url(site, package), | |
| 288 | + | }) | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | /// A version as the API shows one. | |
| 292 | + | pub fn version_json(version: &PackageVersion) -> Value { | |
| 293 | + | json!({ | |
| 294 | + | "id": version.id, | |
| 295 | + | "name": version.version, | |
| 296 | + | "digest": version.digest, | |
| 297 | + | "size_in_bytes": version.size, | |
| 298 | + | "download_count": version.downloads.unwrap_or(0), | |
| 299 | + | "tags": version.tags, | |
| 300 | + | "media_type": version.media_type, | |
| 301 | + | "artifact_type": version.artifact_type, | |
| 302 | + | "subject": version.subject, | |
| 303 | + | "platforms": version.platforms, | |
| 304 | + | "published_by": version.published_by, | |
| 305 | + | "created_at": version.published_at, | |
| 306 | + | "deprecated": version.deprecated, | |
| 307 | + | "deleted_at": version.deleted_at, | |
| 308 | + | "deleted_by": version.deleted_by, | |
| 309 | + | "purge_at": version.purge_at, | |
| 310 | + | }) | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | fn access_json(access: &[PackageAccess]) -> Value { | |
| 314 | + | Value::Array( | |
| 315 | + | access | |
| 316 | + | .iter() | |
| 317 | + | .map(|entry| json!({ "type": entry.kind.as_str(), "id": entry.id, "name": entry.name, "role": entry.role.as_str(), "created_at": entry.created_at })) | |
| 318 | + | .collect(), | |
| 319 | + | ) | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | fn actions_json(access: &[ActionsAccess]) -> Value { | |
| 323 | + | Value::Array( | |
| 324 | + | access | |
| 325 | + | .iter() | |
| 326 | + | .map(|entry| json!({ "repository_id": entry.repo_id, "repository": entry.repo, "role": entry.role.as_str(), "linked": entry.linked, "created_at": entry.created_at })) | |
| 327 | + | .collect(), | |
| 328 | + | ) | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | fn mapped<T>(outcome: Outcome<T>, f: impl FnOnce(T) -> Value) -> Outcome<Value> { | |
| 332 | + | match outcome { | |
| 333 | + | Outcome::Ok(value) => Outcome::Ok(f(value)), | |
| 334 | + | Outcome::Fail(refused) => Outcome::Fail(refused), | |
| 335 | + | } | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | async fn call<T: DeserializeOwned>(services: &Services, method: &str, args: &impl Serialize) -> Result<Outcome<T>> { | |
| 339 | + | g1t_kit::call(&services.packages, method, args).await | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | /// The person making a change, or the refusal for nobody. | |
| 343 | + | fn actor(viewer: &Viewer) -> std::result::Result<User, Outcome<Value>> { | |
| 344 | + | viewer.clone().ok_or_else(|| Outcome::fail(FailureCode::Unauthenticated, "This needs a g1t access token.")) | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | pub async fn run(op: PackagesOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> { | |
| 348 | + | let site = services.addresses.site.clone(); | |
| 349 | + | let Some(workspace) = text(input, "workspace").map(|w| w.to_lowercase()) else { | |
| 350 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the workspace's slug.")); | |
| 351 | + | }; | |
| 352 | + | if op == PackagesOp::ListPackages { | |
| 353 | + | let ecosystem = match text(input, "package_type") { | |
| 354 | + | Some(given) => match ecosystem(&given) { | |
| 355 | + | Some(found) => Some(found), | |
| 356 | + | None => return Ok(Outcome::fail(FailureCode::Invalid, format!("{given} is not a package type: container, npm, cargo, maven, nuget, rubygems or composer."))), | |
| 357 | + | }, | |
| 358 | + | None => None, | |
| 359 | + | }; | |
| 360 | + | let found: Outcome<Vec<PackageSummary>> = if text(input, "state").as_deref() == Some("deleted") { | |
| 361 | + | call(services, "deleted_packages", &DeletedPackagesArgs { workspace, viewer: viewer.clone() }).await? | |
| 362 | + | } else { | |
| 363 | + | let args = ListPackagesArgs { workspace, viewer: viewer.clone(), ecosystem, repo_id: None, query: text(input, "q") }; | |
| 364 | + | call(services, "list_packages", &args).await? | |
| 365 | + | }; | |
| 366 | + | return Ok(mapped(found, |list| { | |
| 367 | + | Value::Array(list.iter().filter(|p| ecosystem.is_none_or(|e| e == p.ecosystem)).map(|p| package_json(p, &site)).collect()) | |
| 368 | + | })); | |
| 369 | + | } | |
| 370 | + | let Some(given) = text(input, "package_type") else { | |
| 371 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the package_type: container, npm, cargo, maven, nuget, rubygems or composer.")); | |
| 372 | + | }; | |
| 373 | + | let Some(ecosystem) = ecosystem(&given) else { | |
| 374 | + | return Ok(Outcome::fail(FailureCode::Invalid, format!("{given} is not a package type: container, npm, cargo, maven, nuget, rubygems or composer."))); | |
| 375 | + | }; | |
| 376 | + | let Some(name) = text(input, "package_name") else { | |
| 377 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the package_name.")); | |
| 378 | + | }; | |
| 379 | + | let surface = Some(services.audit.surface); | |
| 380 | + | let version = text(input, "version_id").unwrap_or_default(); | |
| 381 | + | if matches!(op, PackagesOp::GetVersion | PackagesOp::DeleteVersion | PackagesOp::RestoreVersion) && version.is_empty() { | |
| 382 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the version_id: the version's id, version, digest or a tag.")); | |
| 383 | + | } | |
| 384 | + | let changed = |outcome: Outcome<PackageSummary>| mapped(outcome, |p| package_json(&p, &site)); | |
| 385 | + | macro_rules! actor { | |
| 386 | + | () => { | |
| 387 | + | match actor(viewer) { | |
| 388 | + | Ok(actor) => actor, | |
| 389 | + | Err(refused) => return Ok(refused), | |
| 390 | + | } | |
| 391 | + | }; | |
| 392 | + | } | |
| 393 | + | Ok(match op { | |
| 394 | + | PackagesOp::ListPackages => unreachable!("answered above"), | |
| 395 | + | PackagesOp::GetPackage => { | |
| 396 | + | let found: Outcome<PackageDetail> = call(services, "get_package", &GetPackageArgs { workspace, ecosystem, name, viewer: viewer.clone() }).await?; | |
| 397 | + | mapped(found, |detail| package_json(&detail.package, &site)) | |
| 398 | + | } | |
| 399 | + | PackagesOp::ListVersions => { | |
| 400 | + | let deleted = text(input, "state").as_deref() == Some("deleted"); | |
| 401 | + | let found: Outcome<Vec<PackageVersion>> = | |
| 402 | + | call(services, "list_versions", &ListVersionsArgs { workspace, ecosystem, name, viewer: viewer.clone(), deleted }).await?; | |
| 403 | + | mapped(found, |list| Value::Array(list.iter().map(version_json).collect())) | |
| 404 | + | } | |
| 405 | + | PackagesOp::GetVersion => { | |
| 406 | + | let found: Outcome<PackageVersion> = call(services, "get_version", &GetVersionArgs { workspace, ecosystem, name, viewer: viewer.clone(), version }).await?; | |
| 407 | + | mapped(found, |v| version_json(&v)) | |
| 408 | + | } | |
| 409 | + | PackagesOp::ListAccess | PackagesOp::ListActionsAccess => { | |
| 410 | + | let found: Outcome<PackageSettings> = | |
| 411 | + | call(services, "package_settings", &PackageSettingsArgs { workspace, ecosystem, name, viewer: viewer.clone() }).await?; | |
| 412 | + | mapped(found, |settings| { | |
| 413 | + | if op == PackagesOp::ListAccess { | |
| 414 | + | json!({ "inherit_access": settings.package.inherit_access, "access": access_json(&settings.access) }) | |
| 415 | + | } else { | |
| 416 | + | json!({ "repositories": actions_json(&settings.actions_access) }) | |
| 417 | + | } | |
| 418 | + | }) | |
| 419 | + | } | |
| 420 | + | PackagesOp::UpdatePackage => { | |
| 421 | + | let visibility = match text(input, "visibility").as_deref() { | |
| 422 | + | None => None, | |
| 423 | + | Some("public") => Some(Visibility::Public), | |
| 424 | + | Some("private") => Some(Visibility::Private), | |
| 425 | + | Some(other) => return Ok(Outcome::fail(FailureCode::Invalid, format!("{other} is not a visibility: public or private."))), | |
| 426 | + | }; | |
| 427 | + | let inherit_access = input["inherit_access"].as_bool(); | |
| 428 | + | if visibility.is_none() && inherit_access.is_none() { | |
| 429 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give visibility or inherit_access.")); | |
| 430 | + | } | |
| 431 | + | let args = SetPackageArgs { actor: actor!(), workspace, ecosystem, name, visibility, link: None, unlink: false, inherit_access, surface }; | |
| 432 | + | changed(call(services, "set_package", &args).await?) | |
| 433 | + | } | |
| 434 | + | PackagesOp::LinkPackage => { | |
| 435 | + | let Some(repository) = text(input, "repository") else { | |
| 436 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository: its name, or owner/name.")); | |
| 437 | + | }; | |
| 438 | + | let args = SetPackageArgs { actor: actor!(), workspace, ecosystem, name, visibility: None, link: Some(repository), unlink: false, inherit_access: None, surface }; | |
| 439 | + | changed(call(services, "set_package", &args).await?) | |
| 440 | + | } | |
| 441 | + | PackagesOp::UnlinkPackage => { | |
| 442 | + | let args = SetPackageArgs { actor: actor!(), workspace, ecosystem, name, visibility: None, link: None, unlink: true, inherit_access: None, surface }; | |
| 443 | + | changed(call(services, "set_package", &args).await?) | |
| 444 | + | } | |
| 445 | + | PackagesOp::SetAccess => { | |
| 446 | + | let Some(role) = text(input, "role").and_then(|r| PackageRole::parse(&r)) else { | |
| 447 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the role: read, write or admin.")); | |
| 448 | + | }; | |
| 449 | + | let user = text(input, "username").or_else(|| text(input, "user")); | |
| 450 | + | let args = SetPackageAccessArgs { actor: actor!(), workspace, ecosystem, name, user, team: text(input, "team"), role, surface }; | |
| 451 | + | mapped(call(services, "set_package_access", &args).await?, |list: Vec<PackageAccess>| access_json(&list)) | |
| 452 | + | } | |
| 453 | + | PackagesOp::RemoveAccess => { | |
| 454 | + | let user = text(input, "username").or_else(|| text(input, "user")); | |
| 455 | + | let args = RemovePackageAccessArgs { actor: actor!(), workspace, ecosystem, name, user, team: text(input, "team"), surface }; | |
| 456 | + | mapped(call(services, "remove_package_access", &args).await?, |list: Vec<PackageAccess>| access_json(&list)) | |
| 457 | + | } | |
| 458 | + | PackagesOp::SetActionsAccess => { | |
| 459 | + | let Some(repo) = text(input, "repository") else { | |
| 460 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository: its name, or owner/name.")); | |
| 461 | + | }; | |
| 462 | + | let role = match text(input, "role").and_then(|r| PackageRole::parse(&r)) { | |
| 463 | + | Some(role @ (PackageRole::Read | PackageRole::Write)) => role, | |
| 464 | + | _ => return Ok(Outcome::fail(FailureCode::Invalid, "Give the role: read or write.")), | |
| 465 | + | }; | |
| 466 | + | let args = SetActionsAccessArgs { actor: actor!(), workspace, ecosystem, name, repo, role, surface }; | |
| 467 | + | mapped(call(services, "set_actions_access", &args).await?, |list: Vec<ActionsAccess>| json!({ "repositories": actions_json(&list) })) | |
| 468 | + | } | |
| 469 | + | PackagesOp::RemoveActionsAccess => { | |
| 470 | + | let Some(repo) = text(input, "repository") else { | |
| 471 | + | return Ok(Outcome::fail(FailureCode::Invalid, "Give the repository: its name, or owner/name.")); | |
| 472 | + | }; | |
| 473 | + | let args = RemoveActionsAccessArgs { actor: actor!(), workspace, ecosystem, name, repo, surface }; | |
| 474 | + | mapped(call(services, "remove_actions_access", &args).await?, |list: Vec<ActionsAccess>| json!({ "repositories": actions_json(&list) })) | |
| 475 | + | } | |
| 476 | + | PackagesOp::DeletePackage => { | |
| 477 | + | let args = DeletePackageArgs { actor: actor!(), workspace, ecosystem, name, surface }; | |
| 478 | + | mapped(call::<()>(services, "delete_package", &args).await?, |_| json!({ "deleted": true })) | |
| 479 | + | } | |
| 480 | + | PackagesOp::RestorePackage => { | |
| 481 | + | let args = RestorePackageArgs { actor: actor!(), workspace, ecosystem, name, surface }; | |
| 482 | + | changed(call(services, "restore_package", &args).await?) | |
| 483 | + | } | |
| 484 | + | PackagesOp::DeleteVersion => { | |
| 485 | + | let args = DeleteVersionArgs { actor: actor!(), workspace, ecosystem, name, version, surface }; | |
| 486 | + | mapped(call::<()>(services, "delete_version", &args).await?, |_| json!({ "deleted": true })) | |
| 487 | + | } | |
| 488 | + | PackagesOp::RestoreVersion => { | |
| 489 | + | let args = RestoreVersionArgs { actor: actor!(), workspace, ecosystem, name, version, surface }; | |
| 490 | + | mapped(call(services, "restore_version", &args).await?, |v: PackageVersion| version_json(&v)) | |
| 491 | + | } | |
| 492 | + | }) | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | #[cfg(test)] | |
| 496 | + | mod tests { | |
| 497 | + | use super::*; | |
| 498 | + | ||
| 499 | + | fn summary() -> PackageSummary { | |
| 500 | + | PackageSummary { | |
| 501 | + | id: "pkg_1".into(), | |
| 502 | + | workspace: "acme".into(), | |
| 503 | + | ecosystem: Ecosystem::Container, | |
| 504 | + | name: "web/worker".into(), | |
| 505 | + | address: "g1t.sh/acme/web/worker".into(), | |
| 506 | + | visibility: Visibility::Private, | |
| 507 | + | repo: Some(LinkedRepo { id: "rep_1".into(), namespace: "acme".into(), name: "web".into() }), | |
| 508 | + | description: None, | |
| 509 | + | versions: 3, | |
| 510 | + | latest: Some("latest".into()), | |
| 511 | + | size: 1024, | |
| 512 | + | downloads: 7, | |
| 513 | + | created_at: "2026-10-01T00:00:00.000Z".into(), | |
| 514 | + | updated_at: "2026-10-02T00:00:00.000Z".into(), | |
| 515 | + | inherit_access: true, | |
| 516 | + | deleted_at: None, | |
| 517 | + | deleted_by: None, | |
| 518 | + | purge_at: None, | |
| 519 | + | } | |
| 520 | + | } | |
| 521 | + | ||
| 522 | + | #[test] | |
| 523 | + | fn a_package_is_snake_case_with_its_type_and_page() { | |
| 524 | + | let shown = package_json(&summary(), "https://g1t.sh/"); | |
| 525 | + | assert_eq!(shown["package_type"], "container"); | |
| 526 | + | assert_eq!(shown["repository"]["full_name"], "acme/web"); | |
| 527 | + | assert_eq!(shown["html_url"], "https://g1t.sh/acme/-/packages/container/web/worker"); | |
| 528 | + | assert_eq!(shown["download_count"], 7); | |
| 529 | + | assert!(g1t_kit::wire::camel_case_keys(&shown).is_empty()); | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | #[test] | |
| 533 | + | fn package_types_are_read_as_github_writes_them() { | |
| 534 | + | assert_eq!(ecosystem("docker"), Some(Ecosystem::Container)); | |
| 535 | + | assert_eq!(ecosystem("NuGet"), Some(Ecosystem::Nuget)); | |
| 536 | + | assert_eq!(ecosystem("go"), None, "Go modules are read from git, not managed here"); | |
| 537 | + | assert_eq!(ecosystem("pypi"), None); | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | #[test] | |
| 541 | + | fn each_operation_is_described_with_a_schema_and_a_scope() { | |
| 542 | + | use g1t_contracts::scopes::{Level, scope_for}; | |
| 543 | + | for op in PackagesOp::ALL { | |
| 544 | + | assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Packages(op)), "{}", op.name()); | |
| 545 | + | assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name()); | |
| 546 | + | assert!(op.input()["required"].as_array().unwrap().contains(&json!("workspace")), "{}", op.name()); | |
| 547 | + | let level = scope_for(op.name()).unwrap().level(); | |
| 548 | + | assert_eq!(op.writes(), level != Level::Read, "{}", op.name()); | |
| 549 | + | } | |
| 550 | + | } | |
| 551 | + | } |
| 11388 | 11388 | "maximum_allowed_days": 90 | |
| 11389 | 11389 | }, | |
| 11390 | 11390 | "notes": "Only artifacts uploaded afterwards are kept the new number of days. A workflow's `retention-days` asks for fewer, never more." | |
| 11391 | + | }, | |
| 11392 | + | "list_packages": { | |
| 11393 | + | "params": { | |
| 11394 | + | "workspace": "acme" | |
| 11395 | + | }, | |
| 11396 | + | "query": { | |
| 11397 | + | "package_type": "container" | |
| 11398 | + | }, | |
| 11399 | + | "response": [ | |
| 11400 | + | { | |
| 11401 | + | "id": "pkg_01kq7b3d5f7h9k1m3p5r7t9v1x", | |
| 11402 | + | "name": "web", | |
| 11403 | + | "package_type": "container", | |
| 11404 | + | "workspace": "acme", | |
| 11405 | + | "address": "g1t.sh/acme/web", | |
| 11406 | + | "visibility": "private", | |
| 11407 | + | "repository": { | |
| 11408 | + | "id": "rep_01kpw0a2c4e6g8j0m2p4r6t8v0", | |
| 11409 | + | "name": "web", | |
| 11410 | + | "full_name": "acme/web" | |
| 11411 | + | }, | |
| 11412 | + | "description": null, | |
| 11413 | + | "version_count": 12, | |
| 11414 | + | "latest": "latest", | |
| 11415 | + | "size_in_bytes": 48211932, | |
| 11416 | + | "download_count": 1840, | |
| 11417 | + | "inherit_access": true, | |
| 11418 | + | "created_at": "2026-09-14T10:02:11.000Z", | |
| 11419 | + | "updated_at": "2026-10-08T14:05:20.000Z", | |
| 11420 | + | "deleted_at": null, | |
| 11421 | + | "deleted_by": null, | |
| 11422 | + | "purge_at": null, | |
| 11423 | + | "html_url": "https://g1t.sh/acme/-/packages/container/web" | |
| 11424 | + | } | |
| 11425 | + | ], | |
| 11426 | + | "notes": "Only packages you may pull are listed. With `state=deleted`, the workspace's deleted packages you administer that can still be restored, each with `deleted_at`, `deleted_by` and `purge_at`." | |
| 11427 | + | }, | |
| 11428 | + | "get_package": { | |
| 11429 | + | "params": { | |
| 11430 | + | "workspace": "acme", | |
| 11431 | + | "package_type": "npm", | |
| 11432 | + | "package_name": "ui" | |
| 11433 | + | }, | |
| 11434 | + | "response": { | |
| 11435 | + | "id": "pkg_01kq7b4e6g8j0m2p4r6t8v0x2z", | |
| 11436 | + | "name": "ui", | |
| 11437 | + | "package_type": "npm", | |
| 11438 | + | "workspace": "acme", | |
| 11439 | + | "address": "g1t.sh/-/npm/@acme/ui", | |
| 11440 | + | "visibility": "public", | |
| 11441 | + | "repository": { | |
| 11442 | + | "id": "rep_01kpw0b3d5f7h9k1m3p5r7t9v1", | |
| 11443 | + | "name": "ui", | |
| 11444 | + | "full_name": "acme/ui" | |
| 11445 | + | }, | |
| 11446 | + | "description": "Acme's design system components.", | |
| 11447 | + | "version_count": 4, | |
| 11448 | + | "latest": "2.1.0", | |
| 11449 | + | "size_in_bytes": 391204, | |
| 11450 | + | "download_count": 9213, | |
| 11451 | + | "inherit_access": true, | |
| 11452 | + | "created_at": "2026-08-02T09:30:00.000Z", | |
| 11453 | + | "updated_at": "2026-10-07T16:44:02.000Z", | |
| 11454 | + | "deleted_at": null, | |
| 11455 | + | "deleted_by": null, | |
| 11456 | + | "purge_at": null, | |
| 11457 | + | "html_url": "https://g1t.sh/acme/-/packages/npm/ui" | |
| 11458 | + | }, | |
| 11459 | + | "notes": "A name with a slash is one URL-encoded path segment: `/workspaces/acme/packages/container/web%2Fworker`. Not found when you may not pull it." | |
| 11460 | + | }, | |
| 11461 | + | "update_package": { | |
| 11462 | + | "params": { | |
| 11463 | + | "workspace": "acme", | |
| 11464 | + | "package_type": "container", | |
| 11465 | + | "package_name": "web" | |
| 11466 | + | }, | |
| 11467 | + | "request": { | |
| 11468 | + | "inherit_access": false | |
| 11469 | + | }, | |
| 11470 | + | "response": { | |
| 11471 | + | "id": "pkg_01kq7b3d5f7h9k1m3p5r7t9v1x", | |
| 11472 | + | "name": "web", | |
| 11473 | + | "package_type": "container", | |
| 11474 | + | "workspace": "acme", | |
| 11475 | + | "address": "g1t.sh/acme/web", | |
| 11476 | + | "visibility": "private", | |
| 11477 | + | "repository": { | |
| 11478 | + | "id": "rep_01kpw0a2c4e6g8j0m2p4r6t8v0", | |
| 11479 | + | "name": "web", | |
| 11480 | + | "full_name": "acme/web" | |
| 11481 | + | }, | |
| 11482 | + | "description": null, | |
| 11483 | + | "version_count": 12, | |
| 11484 | + | "latest": "latest", | |
| 11485 | + | "size_in_bytes": 48211932, | |
| 11486 | + | "download_count": 1840, | |
| 11487 | + | "inherit_access": false, | |
| 11488 | + | "created_at": "2026-09-14T10:02:11.000Z", | |
| 11489 | + | "updated_at": "2026-10-08T15:01:40.000Z", | |
| 11490 | + | "deleted_at": null, | |
| 11491 | + | "deleted_by": null, | |
| 11492 | + | "purge_at": null, | |
| 11493 | + | "html_url": "https://g1t.sh/acme/-/packages/container/web" | |
| 11494 | + | }, | |
| 11495 | + | "notes": "With `inherit_access` off, the repository's roles no longer reach the package: only the roles given on it (`set_package_access`) and the workspace's owners. `visibility` is for an unlinked package; a linked one has its repository's." | |
| 11496 | + | }, | |
| 11497 | + | "delete_package": { | |
| 11498 | + | "params": { | |
| 11499 | + | "workspace": "acme", | |
| 11500 | + | "package_type": "container", | |
| 11501 | + | "package_name": "web-old" | |
| 11502 | + | }, | |
| 11503 | + | "response": { | |
| 11504 | + | "deleted": true | |
| 11505 | + | }, | |
| 11506 | + | "notes": "The package and every version are gone from the registry at once. Restore it with `restore_package` within 30 days; until then nobody can publish a package of its name." | |
| 11507 | + | }, | |
| 11508 | + | "restore_package": { | |
| 11509 | + | "params": { | |
| 11510 | + | "workspace": "acme", | |
| 11511 | + | "package_type": "container", | |
| 11512 | + | "package_name": "web-old" | |
| 11513 | + | }, | |
| 11514 | + | "response": { | |
| 11515 | + | "id": "pkg_01kq7b5f7h9k1m3p5r7t9v1x3z", | |
| 11516 | + | "name": "web-old", | |
| 11517 | + | "package_type": "container", | |
| 11518 | + | "workspace": "acme", | |
| 11519 | + | "address": "g1t.sh/acme/web-old", | |
| 11520 | + | "visibility": "private", | |
| 11521 | + | "repository": null, | |
| 11522 | + | "description": null, | |
| 11523 | + | "version_count": 3, | |
| 11524 | + | "latest": "latest", | |
| 11525 | + | "size_in_bytes": 20112840, | |
| 11526 | + | "download_count": 112, | |
| 11527 | + | "inherit_access": true, | |
| 11528 | + | "created_at": "2026-07-01T08:00:00.000Z", | |
| 11529 | + | "updated_at": "2026-10-08T15:10:00.000Z", | |
| 11530 | + | "deleted_at": null, | |
| 11531 | + | "deleted_by": null, | |
| 11532 | + | "purge_at": null, | |
| 11533 | + | "html_url": "https://g1t.sh/acme/-/packages/container/web-old" | |
| 11534 | + | } | |
| 11535 | + | }, | |
| 11536 | + | "list_package_versions": { | |
| 11537 | + | "params": { | |
| 11538 | + | "workspace": "acme", | |
| 11539 | + | "package_type": "container", | |
| 11540 | + | "package_name": "web" | |
| 11541 | + | }, | |
| 11542 | + | "response": [ | |
| 11543 | + | { | |
| 11544 | + | "id": "ver_01kq9c5f7h9k1m3p5r7t9v1x3z", | |
| 11545 | + | "name": "sha256:9b2e4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b", | |
| 11546 | + | "digest": "sha256:9b2e4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b", | |
| 11547 | + | "size_in_bytes": 48211932, | |
| 11548 | + | "download_count": 611, | |
| 11549 | + | "tags": [ | |
| 11550 | + | "latest", | |
| 11551 | + | "v1.4.0" | |
| 11552 | + | ], | |
| 11553 | + | "media_type": "application/vnd.oci.image.index.v1+json", | |
| 11554 | + | "artifact_type": null, | |
| 11555 | + | "subject": null, | |
| 11556 | + | "platforms": [ | |
| 11557 | + | "linux/amd64", | |
| 11558 | + | "linux/arm64" | |
| 11559 | + | ], | |
| 11560 | + | "published_by": "ana", | |
| 11561 | + | "created_at": "2026-10-08T14:05:20.000Z", | |
| 11562 | + | "deprecated": null, | |
| 11563 | + | "deleted_at": null, | |
| 11564 | + | "deleted_by": null, | |
| 11565 | + | "purge_at": null | |
| 11566 | + | } | |
| 11567 | + | ], | |
| 11568 | + | "notes": "A container image's versions are its manifests, named by digest; every other package's by version. `download_count` counts pulls and downloads of the version itself, approximately. With `state=deleted`, the deleted versions that can still be restored, for the package's admins." | |
| 11569 | + | }, | |
| 11570 | + | "get_package_version": { | |
| 11571 | + | "params": { | |
| 11572 | + | "workspace": "acme", | |
| 11573 | + | "package_type": "npm", | |
| 11574 | + | "package_name": "ui", | |
| 11575 | + | "version_id": "2.1.0" | |
| 11576 | + | }, | |
| 11577 | + | "response": { | |
| 11578 | + | "id": "ver_01kq9c6g8j0m2p4r6t8v0x2z4b", | |
| 11579 | + | "name": "2.1.0", | |
| 11580 | + | "digest": "sha256:3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e", | |
| 11581 | + | "size_in_bytes": 97412, | |
| 11582 | + | "download_count": 2204, | |
| 11583 | + | "tags": [ | |
| 11584 | + | "latest" | |
| 11585 | + | ], | |
| 11586 | + | "media_type": null, | |
| 11587 | + | "artifact_type": null, | |
| 11588 | + | "subject": null, | |
| 11589 | + | "platforms": [], | |
| 11590 | + | "published_by": "bo", | |
| 11591 | + | "created_at": "2026-10-07T16:44:02.000Z", | |
| 11592 | + | "deprecated": null, | |
| 11593 | + | "deleted_at": null, | |
| 11594 | + | "deleted_by": null, | |
| 11595 | + | "purge_at": null | |
| 11596 | + | }, | |
| 11597 | + | "notes": "`version_id` is the version's id (`ver_…`), its version, its digest, or a tag that points to it." | |
| 11598 | + | }, | |
| 11599 | + | "delete_package_version": { | |
| 11600 | + | "params": { | |
| 11601 | + | "workspace": "acme", | |
| 11602 | + | "package_type": "npm", | |
| 11603 | + | "package_name": "ui", | |
| 11604 | + | "version_id": "2.0.0-beta.1" | |
| 11605 | + | }, | |
| 11606 | + | "response": { | |
| 11607 | + | "deleted": true | |
| 11608 | + | }, | |
| 11609 | + | "notes": "The version and its tags are gone from the registry at once. It can be restored for 30 days, and its version cannot be published again until it is purged." | |
| 11610 | + | }, | |
| 11611 | + | "restore_package_version": { | |
| 11612 | + | "params": { | |
| 11613 | + | "workspace": "acme", | |
| 11614 | + | "package_type": "npm", | |
| 11615 | + | "package_name": "ui", | |
| 11616 | + | "version_id": "2.0.0-beta.1" | |
| 11617 | + | }, | |
| 11618 | + | "response": { | |
| 11619 | + | "id": "ver_01kq9c7h9k1m3p5r7t9v1x3z5c", | |
| 11620 | + | "name": "2.0.0-beta.1", | |
| 11621 | + | "digest": "sha256:7d9f2a4c6e8b0d1f3a5c7e9b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f", | |
| 11622 | + | "size_in_bytes": 96880, | |
| 11623 | + | "download_count": 41, | |
| 11624 | + | "tags": [ | |
| 11625 | + | "beta" | |
| 11626 | + | ], | |
| 11627 | + | "media_type": null, | |
| 11628 | + | "artifact_type": null, | |
| 11629 | + | "subject": null, | |
| 11630 | + | "platforms": [], | |
| 11631 | + | "published_by": "bo", | |
| 11632 | + | "created_at": "2026-09-30T11:20:00.000Z", | |
| 11633 | + | "deprecated": null, | |
| 11634 | + | "deleted_at": null, | |
| 11635 | + | "deleted_by": null, | |
| 11636 | + | "purge_at": null | |
| 11637 | + | }, | |
| 11638 | + | "notes": "Tags that pointed to it come back with it, unless one was moved to another version meanwhile." | |
| 11639 | + | }, | |
| 11640 | + | "link_package": { | |
| 11641 | + | "params": { | |
| 11642 | + | "workspace": "acme", | |
| 11643 | + | "package_type": "container", | |
| 11644 | + | "package_name": "tools" | |
| 11645 | + | }, | |
| 11646 | + | "request": { | |
| 11647 | + | "repository": "acme/infra" | |
| 11648 | + | }, | |
| 11649 | + | "response": { | |
| 11650 | + | "id": "pkg_01kq7b6g8j0m2p4r6t8v0x2z4b", | |
| 11651 | + | "name": "tools", | |
| 11652 | + | "package_type": "container", | |
| 11653 | + | "workspace": "acme", | |
| 11654 | + | "address": "g1t.sh/acme/tools", | |
| 11655 | + | "visibility": "private", | |
| 11656 | + | "repository": { | |
| 11657 | + | "id": "rep_01kpw0c4e6g8j0m2p4r6t8v0x2", | |
| 11658 | + | "name": "infra", | |
| 11659 | + | "full_name": "acme/infra" | |
| 11660 | + | }, | |
| 11661 | + | "description": null, | |
| 11662 | + | "version_count": 2, | |
| 11663 | + | "latest": "latest", | |
| 11664 | + | "size_in_bytes": 7340032, | |
| 11665 | + | "download_count": 88, | |
| 11666 | + | "inherit_access": true, | |
| 11667 | + | "created_at": "2026-09-20T12:00:00.000Z", | |
| 11668 | + | "updated_at": "2026-10-08T15:20:00.000Z", | |
| 11669 | + | "deleted_at": null, | |
| 11670 | + | "deleted_by": null, | |
| 11671 | + | "purge_at": null, | |
| 11672 | + | "html_url": "https://g1t.sh/acme/-/packages/container/tools" | |
| 11673 | + | }, | |
| 11674 | + | "notes": "The package takes the repository's visibility. You need the Admin role on the package and on the repository." | |
| 11675 | + | }, | |
| 11676 | + | "unlink_package": { | |
| 11677 | + | "params": { | |
| 11678 | + | "workspace": "acme", | |
| 11679 | + | "package_type": "container", | |
| 11680 | + | "package_name": "tools" | |
| 11681 | + | }, | |
| 11682 | + | "response": { | |
| 11683 | + | "id": "pkg_01kq7b6g8j0m2p4r6t8v0x2z4b", | |
| 11684 | + | "name": "tools", | |
| 11685 | + | "package_type": "container", | |
| 11686 | + | "workspace": "acme", | |
| 11687 | + | "address": "g1t.sh/acme/tools", | |
| 11688 | + | "visibility": "private", | |
| 11689 | + | "repository": null, | |
| 11690 | + | "description": null, | |
| 11691 | + | "version_count": 2, | |
| 11692 | + | "latest": "latest", | |
| 11693 | + | "size_in_bytes": 7340032, | |
| 11694 | + | "download_count": 88, | |
| 11695 | + | "inherit_access": true, | |
| 11696 | + | "created_at": "2026-09-20T12:00:00.000Z", | |
| 11697 | + | "updated_at": "2026-10-08T15:22:00.000Z", | |
| 11698 | + | "deleted_at": null, | |
| 11699 | + | "deleted_by": null, | |
| 11700 | + | "purge_at": null, | |
| 11701 | + | "html_url": "https://g1t.sh/acme/-/packages/container/tools" | |
| 11702 | + | } | |
| 11703 | + | }, | |
| 11704 | + | "list_package_access": { | |
| 11705 | + | "params": { | |
| 11706 | + | "workspace": "acme", | |
| 11707 | + | "package_type": "container", | |
| 11708 | + | "package_name": "web" | |
| 11709 | + | }, | |
| 11710 | + | "response": { | |
| 11711 | + | "inherit_access": true, | |
| 11712 | + | "access": [ | |
| 11713 | + | { | |
| 11714 | + | "type": "team", | |
| 11715 | + | "id": "team_01kq1a3c5e7g9j1l3n5q7s9u1w", | |
| 11716 | + | "name": "acme/platform", | |
| 11717 | + | "role": "admin", | |
| 11718 | + | "created_at": "2026-10-01T09:00:00.000Z" | |
| 11719 | + | }, | |
| 11720 | + | { | |
| 11721 | + | "type": "user", | |
| 11722 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09q", | |
| 11723 | + | "name": "dana", | |
| 11724 | + | "role": "read", | |
| 11725 | + | "created_at": "2026-10-02T13:30:00.000Z" | |
| 11726 | + | } | |
| 11727 | + | ] | |
| 11728 | + | }, | |
| 11729 | + | "notes": "Only the roles given on the package itself. With `inherit_access` on, a linked package's repository roles count too, and the workspace's owners administer every package." | |
| 11730 | + | }, | |
| 11731 | + | "set_package_access": { | |
| 11732 | + | "params": { | |
| 11733 | + | "workspace": "acme", | |
| 11734 | + | "package_type": "container", | |
| 11735 | + | "package_name": "web" | |
| 11736 | + | }, | |
| 11737 | + | "request": { | |
| 11738 | + | "username": "dana", | |
| 11739 | + | "role": "write" | |
| 11740 | + | }, | |
| 11741 | + | "response": [ | |
| 11742 | + | { | |
| 11743 | + | "type": "team", | |
| 11744 | + | "id": "team_01kq1a3c5e7g9j1l3n5q7s9u1w", | |
| 11745 | + | "name": "acme/platform", | |
| 11746 | + | "role": "admin", | |
| 11747 | + | "created_at": "2026-10-01T09:00:00.000Z" | |
| 11748 | + | }, | |
| 11749 | + | { | |
| 11750 | + | "type": "user", | |
| 11751 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09q", | |
| 11752 | + | "name": "dana", | |
| 11753 | + | "role": "write", | |
| 11754 | + | "created_at": "2026-10-02T13:30:00.000Z" | |
| 11755 | + | } | |
| 11756 | + | ], | |
| 11757 | + | "notes": "Give `username` or `team` (a team of the workspace, by slug). The role adds to what the repository or workspace already gives." | |
| 11758 | + | }, | |
| 11759 | + | "remove_package_access": { | |
| 11760 | + | "params": { | |
| 11761 | + | "workspace": "acme", | |
| 11762 | + | "package_type": "container", | |
| 11763 | + | "package_name": "web" | |
| 11764 | + | }, | |
| 11765 | + | "query": { | |
| 11766 | + | "username": "dana" | |
| 11767 | + | }, | |
| 11768 | + | "response": [ | |
| 11769 | + | { | |
| 11770 | + | "type": "team", | |
| 11771 | + | "id": "team_01kq1a3c5e7g9j1l3n5q7s9u1w", | |
| 11772 | + | "name": "acme/platform", | |
| 11773 | + | "role": "admin", | |
| 11774 | + | "created_at": "2026-10-01T09:00:00.000Z" | |
| 11775 | + | } | |
| 11776 | + | ], | |
| 11777 | + | "notes": "Over REST, name who in the query: `?username=dana` or `?team=platform`." | |
| 11778 | + | }, | |
| 11779 | + | "list_package_actions_access": { | |
| 11780 | + | "params": { | |
| 11781 | + | "workspace": "acme", | |
| 11782 | + | "package_type": "container", | |
| 11783 | + | "package_name": "web" | |
| 11784 | + | }, | |
| 11785 | + | "response": { | |
| 11786 | + | "repositories": [ | |
| 11787 | + | { | |
| 11788 | + | "repository_id": "rep_01kpw0a2c4e6g8j0m2p4r6t8v0", | |
| 11789 | + | "repository": "acme/web", | |
| 11790 | + | "role": "write", | |
| 11791 | + | "linked": true, | |
| 11792 | + | "created_at": null | |
| 11793 | + | }, | |
| 11794 | + | { | |
| 11795 | + | "repository_id": "rep_01kpw0d5f7h9k1m3p5r7t9v1x3", | |
| 11796 | + | "repository": "acme/deploy", | |
| 11797 | + | "role": "read", | |
| 11798 | + | "linked": false, | |
| 11799 | + | "created_at": "2026-10-03T10:00:00.000Z" | |
| 11800 | + | } | |
| 11801 | + | ] | |
| 11802 | + | }, | |
| 11803 | + | "notes": "A workflow job's `G1T_TOKEN` reaches the package only from these repositories. The linked repository is always listed, with write." | |
| 11804 | + | }, | |
| 11805 | + | "set_package_actions_access": { | |
| 11806 | + | "params": { | |
| 11807 | + | "workspace": "acme", | |
| 11808 | + | "package_type": "container", | |
| 11809 | + | "package_name": "web" | |
| 11810 | + | }, | |
| 11811 | + | "request": { | |
| 11812 | + | "repository": "deploy", | |
| 11813 | + | "role": "read" | |
| 11814 | + | }, | |
| 11815 | + | "response": { | |
| 11816 | + | "repositories": [ | |
| 11817 | + | { | |
| 11818 | + | "repository_id": "rep_01kpw0a2c4e6g8j0m2p4r6t8v0", | |
| 11819 | + | "repository": "acme/web", | |
| 11820 | + | "role": "write", | |
| 11821 | + | "linked": true, | |
| 11822 | + | "created_at": null | |
| 11823 | + | }, | |
| 11824 | + | { | |
| 11825 | + | "repository_id": "rep_01kpw0d5f7h9k1m3p5r7t9v1x3", | |
| 11826 | + | "repository": "acme/deploy", | |
| 11827 | + | "role": "read", | |
| 11828 | + | "linked": false, | |
| 11829 | + | "created_at": "2026-10-03T10:00:00.000Z" | |
| 11830 | + | } | |
| 11831 | + | ] | |
| 11832 | + | } | |
| 11833 | + | }, | |
| 11834 | + | "remove_package_actions_access": { | |
| 11835 | + | "params": { | |
| 11836 | + | "workspace": "acme", | |
| 11837 | + | "package_type": "container", | |
| 11838 | + | "package_name": "web", | |
| 11839 | + | "repository": "deploy" | |
| 11840 | + | }, | |
| 11841 | + | "response": { | |
| 11842 | + | "repositories": [ | |
| 11843 | + | { | |
| 11844 | + | "repository_id": "rep_01kpw0a2c4e6g8j0m2p4r6t8v0", | |
| 11845 | + | "repository": "acme/web", | |
| 11846 | + | "role": "write", | |
| 11847 | + | "linked": true, | |
| 11848 | + | "created_at": null | |
| 11849 | + | } | |
| 11850 | + | ] | |
| 11851 | + | } | |
| 11391 | 11852 | } | |
| 11392 | 11853 | } |
| 125 | 125 | return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is); | |
| 126 | 126 | } | |
| 127 | 127 | Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is), | |
| 128 | + | // Packages are shaped by the API itself, in `snake_case`. | |
| 129 | + | Op::Packages(_) => return as_is, | |
| 128 | 130 | // Built by the API itself, in `snake_case`. | |
| 129 | 131 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 130 | 132 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { |
| 6 | 6 | use crate::artifacts::ArtifactsOp; | |
| 7 | 7 | use crate::deploy_keys::DeployKeysOp; | |
| 8 | 8 | use crate::deployments::DeploymentsOp; | |
| 9 | + | use crate::packages::PackagesOp; | |
| 9 | 10 | use crate::protection::ProtectionOp; | |
| 10 | 11 | use crate::token_policy::TokenOp; | |
| 11 | 12 | use crate::operations::Op; | |
| 700 | 701 | Op::GetJobLogs, | |
| 701 | 702 | &[("after", "after")], | |
| 702 | 703 | ), | |
| 704 | + | // Packages, at GitHub's addresses with the workspace in place of the | |
| 705 | + | // organization. A name with a slash is one URL-encoded segment. | |
| 706 | + | route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]), | |
| 707 | + | route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]), | |
| 708 | + | route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]), | |
| 709 | + | route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]), | |
| 710 | + | route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]), | |
| 711 | + | route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]), | |
| 712 | + | route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]), | |
| 713 | + | route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]), | |
| 714 | + | route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]), | |
| 715 | + | route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]), | |
| 716 | + | route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]), | |
| 717 | + | route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]), | |
| 718 | + | route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]), | |
| 719 | + | route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]), | |
| 720 | + | route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]), | |
| 721 | + | route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]), | |
| 722 | + | route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access/:repository", Op::Packages(PackagesOp::RemoveActionsAccess), &[]), | |
| 703 | 723 | // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to | |
| 704 | 724 | // a signed link (lib.rs). | |
| 705 | 725 | route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]), |
| 22 | 22 | use crate::artifacts::ArtifactsOp; | |
| 23 | 23 | use crate::deploy_keys::DeployKeysOp; | |
| 24 | 24 | use crate::deployments::DeploymentsOp; | |
| 25 | + | use crate::packages::PackagesOp; | |
| 25 | 26 | use crate::protection::ProtectionOp; | |
| 26 | 27 | use crate::token_policy::TokenOp; | |
| 27 | 28 | use crate::operations::Op; | |
| 265 | 266 | ], | |
| 266 | 267 | }, | |
| 267 | 268 | Tool { | |
| 269 | + | name: "package", | |
| 270 | + | title: "Packages", | |
| 271 | + | description: "A workspace's packages in every registry (container images, npm, Cargo, Maven, NuGet, RubyGems, Composer): their versions and downloads, deleting and restoring them within 30 days, their visibility and repository, who has a role on them, and which repositories' workflows may use them (Manage Actions access). Name one by workspace, package_type and package_name.", | |
| 272 | + | default_action: None, | |
| 273 | + | actions: &[ | |
| 274 | + | a("list", Op::Packages(PackagesOp::ListPackages), "A workspace's packages; state deleted for restorable ones"), | |
| 275 | + | a("get", Op::Packages(PackagesOp::GetPackage), "One package: address, visibility, repository, downloads"), | |
| 276 | + | a("versions", Op::Packages(PackagesOp::ListVersions), "Its versions with tags and downloads; state deleted too"), | |
| 277 | + | a("get_version", Op::Packages(PackagesOp::GetVersion), "One version by id, version, digest or tag"), | |
| 278 | + | a("update", Op::Packages(PackagesOp::UpdatePackage), "Set visibility, or inherit_access for a linked one"), | |
| 279 | + | a("link", Op::Packages(PackagesOp::LinkPackage), "Link it to a repository of its workspace"), | |
| 280 | + | a("unlink", Op::Packages(PackagesOp::UnlinkPackage), "Unlink it: the workspace's, private"), | |
| 281 | + | a("access", Op::Packages(PackagesOp::ListAccess), "People and teams with a role on it"), | |
| 282 | + | a("set_access", Op::Packages(PackagesOp::SetAccess), "Give a person or team read, write or admin"), | |
| 283 | + | a("remove_access", Op::Packages(PackagesOp::RemoveAccess), "Take a person's or team's role away"), | |
| 284 | + | a("actions_access", Op::Packages(PackagesOp::ListActionsAccess), "Repositories whose workflows may use it"), | |
| 285 | + | a("set_actions_access", Op::Packages(PackagesOp::SetActionsAccess), "Let a repository's workflows read or write it"), | |
| 286 | + | a("remove_actions_access", Op::Packages(PackagesOp::RemoveActionsAccess), "Stop a repository's workflows using it"), | |
| 287 | + | a("delete", Op::Packages(PackagesOp::DeletePackage), "Delete it; restorable for 30 days"), | |
| 288 | + | a("restore", Op::Packages(PackagesOp::RestorePackage), "Restore a deleted package"), | |
| 289 | + | a("delete_version", Op::Packages(PackagesOp::DeleteVersion), "Delete a version; restorable for 30 days"), | |
| 290 | + | a("restore_version", Op::Packages(PackagesOp::RestoreVersion), "Restore a deleted version"), | |
| 291 | + | ], | |
| 292 | + | }, | |
| 293 | + | Tool { | |
| 268 | 294 | name: "secret", | |
| 269 | 295 | title: "Secrets and variables", | |
| 270 | 296 | description: "A repository's or workspace's secrets and variables, read by workflows and deployments. Secret values are never returned.", | |
| 764 | 790 | assert!(tool.action(default).is_some(), "{}", tool.name); | |
| 765 | 791 | } | |
| 766 | 792 | } | |
| 767 | − | assert!(TOOLS.len() <= 17, "{} tools", TOOLS.len()); | |
| 793 | + | assert!(TOOLS.len() <= 18, "{} tools", TOOLS.len()); | |
| 768 | 794 | } | |
| 769 | 795 | ||
| 770 | 796 | #[test] |
| 31 | 31 | // Secret and dependency alerts: list_security_alerts and dismissing them. | |
| 32 | 32 | { "binding": "SECURITY", "service": "g1t-security" }, | |
| 33 | 33 | // A person's pinned projects: list_pinned_projects and changing them. | |
| 34 | − | { "binding": "PROJECTS", "service": "g1t-projects" } | |
| 34 | + | { "binding": "PROJECTS", "service": "g1t-projects" }, | |
| 35 | + | // Packages: list_packages, their settings, deleting and restoring them. | |
| 36 | + | { "binding": "PACKAGES", "service": "g1t-packages" } | |
| 35 | 37 | ], | |
| 36 | 38 | // GitHub Actions artifacts older runners kept, in chunks, with KV's own | |
| 37 | 39 | // expiry, read until it passes (and cache |
| 232 | 232 | ||
| 233 | 233 | On g1t's machines, a job is signed in to g1t's container registry from | |
| 234 | 234 | the start, with its own `G1T_TOKEN`, so it can push to and pull from its | |
| 235 | − | workspace's images without a login step. A run that gets no secrets is | |
| 235 | + | repository's images without a login step; images of other repositories | |
| 236 | + | need this one added under their | |
| 237 | + | [Manage Actions access](/guides/packages/#manage-actions-access). A run that gets no secrets is | |
| 236 | 238 | not signed in. See [container registry](/guides/containers/#in-workflows). | |
| 237 | 239 | ||
| 238 | 240 | ```yaml | |
| 759 | 761 | [API](/reference/api/) or pushes with git: | |
| 760 | 762 | ||
| 761 | 763 | - It reaches **this repository only**. Every other repository, even one | |
| 762 | − | in the same workspace, is refused. | |
| 764 | + | in the same workspace, is refused. So are packages: it reaches this | |
| 765 | + | repository's own, and another package only once the package's admins | |
| 766 | + | add this repository under its | |
| 767 | + | [Manage Actions access](/guides/packages/#manage-actions-access). | |
| 763 | 768 | - It can do **what its `permissions:` say**, and nothing more. | |
| 764 | 769 | - It **stops working when the job ends**, however it ends. | |
| 765 | 770 | - Everything it changes is in the [audit log](/guides/audit-log/) as that |
| 50 | 50 | | `team.created`, `team.edited`, `team.deleted` | A [team](/guides/teams/) was created, changed or deleted. | | |
| 51 | 51 | | `team.member_added`, `team.member_role_changed`, `team.member_removed` | Someone was added to a team, made its maintainer or a member, or taken out of it. | | |
| 52 | 52 | | `team.repo_added`, `team.repo_role_changed`, `team.repo_removed` | A team was given a role on a repository, had it changed, or lost it. | | |
| 53 | + | | `package.delete`, `package.restore`, `package.purged` | A [package](/guides/packages/#delete-and-restore) was deleted, restored, or removed for good 30 days after it was deleted. | | |
| 54 | + | | `package.delete_version`, `package.restore_version` | One of its versions was deleted or restored. | | |
| 55 | + | | `package.access_added`, `package.access_role_changed`, `package.access_removed` | A person or team was given a role on a package itself, had it changed, or lost it. | | |
| 56 | + | | `package.actions_access_added`, `package.actions_access_role_changed`, `package.actions_access_removed` | A repository's workflows were given access to a package under [Manage Actions access](/guides/packages/#manage-actions-access), had it changed, or lost it. | | |
| 57 | + | | `package.inherit_access_changed` | Inheriting access from the linked repository was turned on or off. | | |
| 58 | + | | `package.visibility_changed` | A package was made public or private. | | |
| 59 | + | | `package.linked`, `package.unlinked` | A package was linked to a repository (from its settings, or by an image's source label), or unlinked. | | |
| 53 | 60 | | `workspace.residency_changed` | An owner changed where the workspace's new repositories are stored. See [data residency](/guides/workspaces/#data-residency). | | |
| 54 | 61 | | `workspace.deleted`, `workspace.restored`, `workspace.purged` | An owner deleted the workspace, g1t's support restored it, or it was removed for good. See [deleting a workspace](/guides/workspaces/#delete-a-workspace). | | |
| 55 | 62 |
| 529 | 529 | | `security:read` | See [secret scanning](/guides/security/secret-protection/), [code scanning](/guides/security/code-scanning/) and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings | | |
| 530 | 530 | | `security:write` | Dismiss and reopen alerts, bypass push protection, review bypass requests, manage custom patterns, upload SARIF and change security settings | | |
| 531 | 531 | | `packages:read` | Pull container images and install private [packages](/guides/packages/). Public ones need no scope. | | |
| 532 | − | | `packages:write` | Push container images and publish packages | | |
| 533 | − | | `packages:delete` | Delete packages and their versions | | |
| 532 | + | | `packages:write` | Push container images and publish packages; with the Admin role on a package, change its settings | | |
| 533 | + | | `packages:delete` | Delete and restore packages and their versions | | |
| 534 | 534 | | `issues:read` | Read issues, comments and plans | | |
| 535 | 535 | | `issues:write` | Open, edit, close and comment on issues | | |
| 536 | 536 | | `pull_requests:read` | Read pull requests, their changes, sessions and merge queues | |
| 38 | 38 | follows its visibility and roles; pushing needs Write on it. Otherwise it is | |
| 39 | 39 | the workspace's, private, and needs the workspace's Write base permission. | |
| 40 | 40 | ||
| 41 | + | ### Link an image with its source label | |
| 42 | + | ||
| 43 | + | An image can name the repository it is built from, whatever the image is | |
| 44 | + | called, with the `org.opencontainers.image.source` label: | |
| 45 | + | ||
| 46 | + | ```dockerfile | |
| 47 | + | LABEL org.opencontainers.image.source=https://g1t.sh/acme/web | |
| 48 | + | ``` | |
| 49 | + | ||
| 50 | + | or as an annotation on the manifest | |
| 51 | + | (`docker buildx build --annotation "org.opencontainers.image.source=https://g1t.sh/acme/web"`). | |
| 52 | + | g1t reads the manifest's annotations first, then the image config's labels. | |
| 53 | + | When the address is a repository of the image's own workspace | |
| 54 | + | (`https://g1t.sh/<workspace>/<repo>`, with or without `.git`) and whoever | |
| 55 | + | pushes has the Write role on that repository, the image is linked to it: | |
| 56 | + | ||
| 57 | + | | When | What happens | | |
| 58 | + | | --- | --- | | |
| 59 | + | | The image's first push | It is linked to the repository the label names, in place of the one its name starts with. | | |
| 60 | + | | A later push of an image that is not linked | It is linked to the repository the label names. | | |
| 61 | + | | A later push of a linked image | Nothing changes. Link it elsewhere from its [settings](/guides/packages/#package-settings). | | |
| 62 | + | | The label names another workspace's repository, another host, or a repository you cannot write to | It is ignored, and the image is linked by its name, as above. | | |
| 63 | + | ||
| 64 | + | Linking gives the image the repository's visibility and, unless its | |
| 65 | + | admins turn inheriting off, its roles. The link is recorded in the | |
| 66 | + | [audit log](/guides/audit-log/) as `package.linked`. | |
| 67 | + | ||
| 41 | 68 | Pushing a tag again moves it to the new image. Layers already on g1t are | |
| 42 | 69 | not uploaded again, and images in the same workspace share them. | |
| 43 | 70 | ||
| 55 | 82 | ||
| 56 | 83 | ## In workflows | |
| 57 | 84 | ||
| 58 | − | A workflow's `G1T_TOKEN`, [the job's own token](/guides/actions/#the-jobs-token), can pull the workspace's images, and | |
| 59 | − | push them with `packages: write` in its [`permissions:`](/guides/actions/#the-jobs-token): | |
| 85 | + | A workflow's `G1T_TOKEN`, [the job's own token](/guides/actions/#the-jobs-token), pulls and pushes the images | |
| 86 | + | linked to its own repository, and with `packages: write` in its | |
| 87 | + | [`permissions:`](/guides/actions/#the-jobs-token) pushes new ones. An image | |
| 88 | + | linked to another repository, or one of the workspace's own that the job | |
| 89 | + | did not make, needs that job's repository added under the image's | |
| 90 | + | [Manage Actions access](/guides/packages/#manage-actions-access): | |
| 60 | 91 | ||
| 61 | 92 | ```yaml | |
| 62 | 93 | jobs: | |
| 201 | 232 | ||
| 202 | 233 | ## Delete | |
| 203 | 234 | ||
| 204 | − | Deleting needs Admin on the linked repository, or for an unlinked image, an | |
| 205 | − | owner of the workspace; a token needs `packages:delete`. | |
| 235 | + | Deleting needs the Admin role on the image (Admin on the linked repository | |
| 236 | + | while it inherits access, a role given on the image itself, or an owner of | |
| 237 | + | the workspace); a token needs `packages:delete`. | |
| 206 | 238 | ||
| 207 | 239 | The registry protocol's `DELETE` removes a tag, or a whole version by its | |
| 208 | − | digest (with every tag that points to it): | |
| 240 | + | digest (with every tag that points to it). A deleted version can be | |
| 241 | + | [restored](/guides/packages/#delete-and-restore) for 30 days, and until | |
| 242 | + | then a manifest with its digest cannot be pushed again: | |
| 209 | 243 | ||
| 210 | 244 | ```sh | |
| 211 | 245 | TOKEN=$(curl -s -u <you>:<token> "https://g1t.sh/v2/token?scope=repository:acme/web:delete" | jq -r .token) | |
| 213 | 247 | curl -X DELETE -H "Authorization: Bearer $TOKEN" https://g1t.sh/v2/acme/web/manifests/sha256:… | |
| 214 | 248 | ``` | |
| 215 | 249 | ||
| 216 | − | Layers no version uses any more are deleted from storage a day later. | |
| 250 | + | A deleted version keeps its layers until it is purged. Layers no version | |
| 251 | + | uses any more are deleted from storage a day later. | |
| 217 | 252 | ||
| 218 | 253 | ## Errors | |
| 219 | 254 |
| 30 | 30 | ||
| 31 | 31 | ## Who can see and publish a package | |
| 32 | 32 | ||
| 33 | − | A package is linked to a repository, or belongs to its workspace. | |
| 33 | + | Everyone has one role on a package, the highest of what reaches them: | |
| 34 | 34 | ||
| 35 | − | - **Linked.** The first push of an image whose name starts with a | |
| 36 | − | repository's name (`acme/web`, `acme/web/worker` for the repository | |
| 37 | − | `acme/web`) links it to that repository; so does the first publish of | |
| 38 | − | an npm package whose `package.json` `repository` is a g1t.sh repository | |
| 39 | − | of the workspace, or which is named like one (`@acme/web`), and of a | |
| 40 | − | crate whose `Cargo.toml` `repository` is one, or which is named like | |
| 41 | − | one. Maven artifacts (by artifactId, or the POM's `<scm><url>`), NuGet | |
| 42 | − | packages (by `RepositoryUrl`, or their id) and gems (by | |
| 43 | − | `source_code_uri`, or their name) are linked the same way. It then has the repository's visibility and [roles](/guides/access-and-roles/): | |
| 35 | + | | Role | Lets them | | |
| 36 | + | | --- | --- | | |
| 37 | + | | Read | Pull and install it. Anyone, signed in or not, may on a public package. | | |
| 38 | + | | Write | Also publish new versions and tags. | | |
| 39 | + | | Admin | Also delete and restore it and its versions, and change its [settings](#package-settings). | | |
| 44 | 40 | ||
| 45 | − | | | Needs | | |
| 46 | − | | --- | --- | | |
| 47 | − | | Pull | Read: on a public repository, anyone, signed in or not | | |
| 48 | − | | Push a new version or tag | Write | | |
| 49 | − | | Delete versions and the package, change its settings | Admin | | |
| 41 | + | A package is linked to a repository, or belongs to its workspace, and that | |
| 42 | + | decides where its roles come from: | |
| 50 | 43 | ||
| 44 | + | - **Linked.** The first push of an image whose name starts with a | |
| 45 | + | repository's name (`acme/web`, `acme/web/worker` for the repository | |
| 46 | + | `acme/web`) links it to that repository, unless its | |
| 47 | + | [source label](/guides/containers/#link-an-image-with-its-source-label) | |
| 48 | + | names another; so does the first publish of an npm package whose | |
| 49 | + | `package.json` `repository` is a g1t.sh repository of the workspace, or | |
| 50 | + | which is named like one (`@acme/web`), and of a crate whose `Cargo.toml` | |
| 51 | + | `repository` is one, or which is named like one. Maven artifacts (by | |
| 52 | + | artifactId, or the POM's `<scm><url>`), NuGet packages (by | |
| 53 | + | `RepositoryUrl`, or their id) and gems (by `source_code_uri`, or their | |
| 54 | + | name) are linked the same way. It has the repository's visibility and, | |
| 55 | + | while it **inherits access** (the default), its | |
| 56 | + | [roles](/guides/access-and-roles/): Read and Triage pull, Write and | |
| 57 | + | Maintain publish, Admin administers. | |
| 51 | 58 | - **Unlinked.** A package whose name matches no repository is the | |
| 52 | 59 | workspace's. It is private: members pull and push it by the workspace's | |
| 53 | 60 | [base permission](/guides/access-and-roles/#the-base-permission) (Read pulls, | |
| 54 | − | Write pushes), and only owners delete it or change its settings. An owner | |
| 61 | + | Write pushes), and an Admin base permission still only pushes. An admin | |
| 55 | 62 | can make it public, and then anyone can pull it. | |
| 56 | 63 | ||
| 57 | − | A linked package can be unlinked, and an unlinked one linked to a | |
| 58 | − | repository of its workspace by someone with Admin on that repository. | |
| 64 | + | On top of that: | |
| 65 | + | ||
| 66 | + | - **The workspace's owners** are admins of every package. | |
| 67 | + | - **People and teams given a role on the package itself**, under | |
| 68 | + | [Manage access](#manage-access), have that role, or the one their | |
| 69 | + | repository or base permission gives them if it is higher. A team's role | |
| 70 | + | reaches its child teams' people too. | |
| 71 | + | - **Turning off inheriting** on a linked package leaves only the roles | |
| 72 | + | given on the package itself, and the workspace's owners: the | |
| 73 | + | repository's roles no longer reach it. Its visibility is still the | |
| 74 | + | repository's. | |
| 59 | 75 | ||
| 60 | 76 | Private packages look exactly like ones that do not exist to anyone who may | |
| 61 | 77 | not pull them. | |
| 62 | 78 | ||
| 79 | + | ## Package settings | |
| 80 | + | ||
| 81 | + | A package's admins see a **Settings** tab on its page, at | |
| 82 | + | `g1t.sh/<workspace>/-/packages/<type>/<name>?tab=settings`: | |
| 83 | + | ||
| 84 | + | | Setting | What it does | | |
| 85 | + | | --- | --- | | |
| 86 | + | | Inherit access from the linked repository | On (the default), the repository's roles reach the package. Off, only the roles below and the workspace's owners do. Linked packages only. | | |
| 87 | + | | Manage access | People (by username) and teams of the workspace (by slug), each with the Read, Write or Admin role on the package. | | |
| 88 | + | | Manage Actions access | The repositories whose workflows may use the package, each with Read or Write. See [below](#manage-actions-access). | | |
| 89 | + | | Visibility | Public or private, for an unlinked package. A linked one has its repository's. | | |
| 90 | + | | Repository | Link it to a repository of its workspace (Admin on that repository is needed too), or unlink it: it is then the workspace's, and private. | | |
| 91 | + | | Deleted versions | Versions deleted in the last 30 days, each with **Restore**. | | |
| 92 | + | | Delete this package | Delete it, with every version. See [deleting and restoring](#delete-and-restore). | | |
| 93 | + | ||
| 94 | + | ### Manage access | |
| 95 | + | ||
| 96 | + | Add a person by username, or a team of the workspace by its slug, with a | |
| 97 | + | role; change a role from its row, or remove it. What a repository or the | |
| 98 | + | workspace gives someone stays when the package's own role is removed. | |
| 99 | + | ||
| 100 | + | ### Manage Actions access | |
| 101 | + | ||
| 102 | + | A workflow job's [`G1T_TOKEN`](/guides/actions/#the-jobs-token) reaches a | |
| 103 | + | package only from these repositories: | |
| 104 | + | ||
| 105 | + | | Repository | Its workflows may | | |
| 106 | + | | --- | --- | | |
| 107 | + | | The one the package is linked to | Pull and publish it, always. It is listed, and cannot be removed: unlink the package instead. | | |
| 108 | + | | One added with Read | Pull it | | |
| 109 | + | | One added with Write | Pull and publish it | | |
| 110 | + | | Any other | Nothing: the job is refused, with a message naming the package and saying to add its repository here. A public package still pulls. | | |
| 111 | + | ||
| 112 | + | A package a workflow job makes that is not linked to the job's repository | |
| 113 | + | (a workspace package, say `g1t.sh/acme/tools` pushed from `acme/web`) is | |
| 114 | + | given its repository with Write, so the workflow that made it keeps | |
| 115 | + | publishing it. The job's token is still held to the job's own | |
| 116 | + | `permissions:`, never deletes a package and never changes its settings. | |
| 117 | + | ||
| 118 | + | ## Delete and restore | |
| 119 | + | ||
| 120 | + | Deleting a package, or one version, hides it from every registry at once: | |
| 121 | + | pulls and installs get "not found", as for one that never existed. It is | |
| 122 | + | kept for **30 days**, and until then: | |
| 123 | + | ||
| 124 | + | - An admin of the package can restore it: a version from the package's | |
| 125 | + | **Settings → Deleted versions**, a package from **Deleted packages** on | |
| 126 | + | the workspace's Packages page (`g1t.sh/<workspace>/-/packages?view=deleted`), | |
| 127 | + | listed for whoever administers it. A restored version comes back with | |
| 128 | + | the tags that still pointed to it. | |
| 129 | + | - A deleted package's name stays taken: nobody can publish a package of | |
| 130 | + | that name in the workspace. A deleted version's version (for a container | |
| 131 | + | image, its digest) cannot be published again. | |
| 132 | + | - Its files are kept, and do not count toward the workspace's storage. | |
| 133 | + | ||
| 134 | + | After 30 days it is purged for good, and files nothing else uses are | |
| 135 | + | removed a day later. Composer versions follow their repository's tags and | |
| 136 | + | branches, so they are deleted there, not here. | |
| 137 | + | ||
| 138 | + | Deleting through a registry's own protocol (`docker` and the OCI `DELETE`, | |
| 139 | + | `npm unpublish`) works the same way. | |
| 140 | + | ||
| 63 | 141 | ## Tokens | |
| 64 | 142 | ||
| 65 | 143 | Sign in to a registry with your username and an | |
| 70 | 148 | | --- | --- | | |
| 71 | 149 | | `packages:read` | Pull private packages. Public ones need no scope. | | |
| 72 | 150 | | `packages:write` | Push and publish. Includes `packages:read`. | | |
| 73 | − | | `packages:delete` | Delete versions and packages | | |
| 151 | + | | `packages:delete` | Delete and restore versions and packages | | |
| 74 | 152 | ||
| 75 | − | Tokens with full access, and tokens made before scopes, have all three. A | |
| 76 | − | token never does more than its owner could: `packages:delete` alone does not | |
| 77 | − | let a member delete an owner's package. | |
| 153 | + | Tokens with full access, and tokens made before scopes, have all three. | |
| 154 | + | Changing a package's settings needs `packages:write` and the Admin role on | |
| 155 | + | it. A token never does more than its owner could: `packages:delete` alone | |
| 156 | + | does not let a member delete an owner's package. | |
| 78 | 157 | ||
| 79 | − | In [workflows](/guides/actions/#secrets-and-variables), `G1T_TOKEN` is the | |
| 80 | − | workspace's own token for the run: it pushes and pulls the workspace's | |
| 81 | − | packages with no setup. A g1t agent at work on a repository may push the | |
| 82 | − | packages of that repository, as it may push its code, and never deletes | |
| 83 | − | them. | |
| 158 | + | | Token | Reaches | | |
| 159 | + | | --- | --- | | |
| 160 | + | | A personal access token | What its owner may do, limited by its scopes. | | |
| 161 | + | | A fine-grained token | Packages linked to a repository in its selection, and the unlinked packages of its resource owner. Elsewhere it pulls public packages only. | | |
| 162 | + | | A workspace's own token | The workspace's packages, as a member with Write; with Admin when an owner gave the token Admin. | | |
| 163 | + | | A workflow job's `G1T_TOKEN` | Its own repository's packages, and those that list its repository under [Manage Actions access](#manage-actions-access), as the job's `permissions:` allow. | | |
| 164 | + | | A deploy key | No packages. | | |
| 84 | 165 | ||
| 166 | + | In [workflows](/guides/actions/#the-jobs-token), `G1T_TOKEN` is the job's | |
| 167 | + | own token: it pushes and pulls its repository's packages with no setup, and | |
| 168 | + | any other package once its admins add the repository under Manage Actions | |
| 169 | + | access. A g1t agent at work on a repository may push the packages of that | |
| 170 | + | repository, as it may push its code, and never deletes them or changes their | |
| 171 | + | settings. | |
| 172 | + | ||
| 85 | 173 | ## Storage | |
| 86 | 174 | ||
| 87 | 175 | Every file is kept once, by its content: two images that share a layer | |
| 97 | 185 | storage past those amounts is charged instead. See | |
| 98 | 186 | [storage and pull limits](/guides/containers/#storage-and-pull-limits). | |
| 99 | 187 | ||
| 188 | + | ## Downloads | |
| 189 | + | ||
| 190 | + | Each package's page shows its pulls or downloads, and each version's own, | |
| 191 | + | counted approximately: an image's manifest fetched by tag or digest, an npm | |
| 192 | + | tarball, a crate, a Maven artifact (not its POM or signatures), a NuGet | |
| 193 | + | `.nupkg`, a gem, and a Composer zip. | |
| 194 | + | ||
| 195 | + | ## The API | |
| 196 | + | ||
| 197 | + | The [REST API](/reference/api/) and the `package` tool of the | |
| 198 | + | [MCP server](/reference/mcp/) manage packages. A package is named by its | |
| 199 | + | type (`container`, `npm`, `cargo`, `maven`, `nuget`, `rubygems` or | |
| 200 | + | `composer`) and its name, URL-encoded where it holds a slash | |
| 201 | + | (`web%2Fworker`): | |
| 202 | + | ||
| 203 | + | | Route | Operation | Scope | | |
| 204 | + | | --- | --- | --- | | |
| 205 | + | | `GET /workspaces/{workspace}/packages` | `list_packages` (`state=deleted` for deleted ones) | `packages:read` | | |
| 206 | + | | `GET /workspaces/{workspace}/packages/{package_type}/{package_name}` | `get_package` | `packages:read` | | |
| 207 | + | | `PATCH …/{package_name}` | `update_package`: `visibility`, `inherit_access` | `packages:write` | | |
| 208 | + | | `DELETE …/{package_name}` | `delete_package` | `packages:delete` | | |
| 209 | + | | `POST …/{package_name}/restore` | `restore_package` | `packages:delete` | | |
| 210 | + | | `GET …/{package_name}/versions` | `list_package_versions` (`state=deleted` for deleted ones) | `packages:read` | | |
| 211 | + | | `GET …/versions/{version_id}` | `get_package_version` | `packages:read` | | |
| 212 | + | | `DELETE …/versions/{version_id}` | `delete_package_version` | `packages:delete` | | |
| 213 | + | | `POST …/versions/{version_id}/restore` | `restore_package_version` | `packages:delete` | | |
| 214 | + | | `PUT …/{package_name}/repository` | `link_package` | `packages:write` | | |
| 215 | + | | `DELETE …/{package_name}/repository` | `unlink_package` | `packages:write` | | |
| 216 | + | | `GET …/{package_name}/access` | `list_package_access` | `packages:read` | | |
| 217 | + | | `PUT …/{package_name}/access` | `set_package_access`: `username` or `team`, and `role` | `packages:write` | | |
| 218 | + | | `DELETE …/{package_name}/access?username=…` or `?team=…` | `remove_package_access` | `packages:write` | | |
| 219 | + | | `GET …/{package_name}/actions-access` | `list_package_actions_access` | `packages:read` | | |
| 220 | + | | `PUT …/{package_name}/actions-access` | `set_package_actions_access`: `repository` and `role` | `packages:write` | | |
| 221 | + | | `DELETE …/{package_name}/actions-access/{repository}` | `remove_package_actions_access` | `packages:write` | | |
| 222 | + | ||
| 223 | + | `version_id` is a version's id (`ver_…`), its version, its digest, or a tag | |
| 224 | + | that points to it. Reading access and deleted versions, and every change, | |
| 225 | + | also needs the Admin role on the package. Bodies and answers are | |
| 226 | + | snake_case. | |
| 227 | + | ||
| 100 | 228 | ## Events and the audit log | |
| 101 | 229 | ||
| 102 | 230 | Publishing a version, deleting a version and deleting a package are | |
| 103 | 231 | [audit log](/guides/audit-log/) entries (so are deprecating an npm version, | |
| 104 | 232 | yanking or unyanking a crate version, unlisting or listing a NuGet version, | |
| 105 | − | pushing a NuGet version's symbols and yanking a gem version), and the events | |
| 233 | + | pushing a NuGet version's symbols and yanking a gem version), as are | |
| 234 | + | restoring them, purging them after 30 days, every change to who has access | |
| 235 | + | and to Manage Actions access, changing the visibility, and linking and | |
| 236 | + | unlinking (see the [audit log's list](/guides/audit-log/)). The events | |
| 106 | 237 | `package.published`, `package.version_deleted`, `package.deleted` and | |
| 107 | 238 | `package.visibility_changed`, which [webhooks](/guides/webhooks/) can be | |
| 108 | 239 | sent: a linked package's go to its repository's webhooks and its |
| 3 | 3 | description: The g1t MCP server's resource tools, each action they take with its required inputs and scope, and how to call them. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | − | The MCP server at `https://mcp.g1t.sh` exposes 17 tools, one per kind of | |
| 6 | + | The MCP server at `https://mcp.g1t.sh` exposes 18 tools, one per kind of | |
| 7 | 7 | thing on g1t: `search`, `repository`, `issue`, `pull_request`, `agent`, | |
| 8 | − | `plan`, `memory`, `workflow`, `secret`, `security`, `webhook`, `access`, | |
| 8 | + | `plan`, `memory`, `workflow`, `package`, `secret`, `security`, `webhook`, `access`, | |
| 9 | 9 | `team`, `workspace`, `billing`, `notifications` and `account`. Each tool takes an `action` that says what to do. Every | |
| 10 | 10 | action is the same operation as a route of the [REST API](/reference/api/), | |
| 11 | 11 | with the same inputs, permissions and results, so the two always agree. | |
| 466 | 466 | | [`get_runner_settings`](/reference/api/runners/get-runner-settings-for-workspace/) | Whether agent work runs on self-hosted runners and on which labels, and whether pull requests from forks may use them. | `workspace` or `repo` | `runners:read` | | |
| 467 | 467 | | [`update_runner_settings`](/reference/api/runners/update-runner-settings-for-workspace/) | Change them: `agents_on_self_hosted`, `agent_labels`, `fork_pull_requests`, or `inherit` for a repository. | `workspace` or `repo` | `runners:admin` | | |
| 468 | 468 | ||
| 469 | + | ## `package` | |
| 470 | + | ||
| 471 | + | A workspace's [packages](/guides/packages/) in every registry: their | |
| 472 | + | versions and downloads, deleting and restoring them within 30 days, their | |
| 473 | + | visibility and repository, the people and teams with a role on them, and | |
| 474 | + | which repositories' workflows may use them. Every action takes | |
| 475 | + | `workspace`; all but `list` also take `package_type` (`container`, `npm`, | |
| 476 | + | `cargo`, `maven`, `nuget`, `rubygems` or `composer`) and `package_name`. | |
| 477 | + | Reading access and every change also need the Admin role on the package. | |
| 478 | + | ||
| 479 | + | | Action | What it does | Required | Scope | | |
| 480 | + | | --- | --- | --- | --- | | |
| 481 | + | | [`list`](/reference/api/packages/list-packages/) | The workspace's packages you may pull, by `package_type` and `q`; with `state` `deleted`, its deleted ones you administer. | `workspace` | `packages:read` | | |
| 482 | + | | [`get`](/reference/api/packages/get-package/) | One package: address, visibility, repository, versions, downloads. | `package_type`, `package_name` | `packages:read` | | |
| 483 | + | | [`versions`](/reference/api/packages/list-package-versions/) | Its versions with tags and downloads, newest first; `state` `deleted` for deleted ones. | `package_type`, `package_name` | `packages:read` | | |
| 484 | + | | [`get_version`](/reference/api/packages/get-package-version/) | One version by id, version, digest or tag. | `version_id` | `packages:read` | | |
| 485 | + | | [`update`](/reference/api/packages/update-package/) | Set `visibility` (unlinked packages), or `inherit_access` (linked ones). | `package_type`, `package_name` | `packages:write` | | |
| 486 | + | | [`link`](/reference/api/packages/link-package/) | Link it to a `repository` of its workspace; Admin on the repository too. | `repository` | `packages:write` | | |
| 487 | + | | [`unlink`](/reference/api/packages/unlink-package/) | Unlink it: the workspace's, private. | `package_type`, `package_name` | `packages:write` | | |
| 488 | + | | [`access`](/reference/api/packages/list-package-access/) | People and teams with a role on it, and `inherit_access`. | `package_type`, `package_name` | `packages:read` | | |
| 489 | + | | [`set_access`](/reference/api/packages/set-package-access/) | Give `username` or `team` a `role`: `read`, `write` or `admin`. | `role` | `packages:write` | | |
| 490 | + | | [`remove_access`](/reference/api/packages/remove-package-access/) | Take `username`'s or `team`'s role away. | `package_type`, `package_name` | `packages:write` | | |
| 491 | + | | [`actions_access`](/reference/api/packages/list-package-actions-access/) | Repositories whose workflows may use it; the linked one is always listed. | `package_type`, `package_name` | `packages:read` | | |
| 492 | + | | [`set_actions_access`](/reference/api/packages/set-package-actions-access/) | Let a `repository`'s workflows use it with `role` `read` or `write`. | `repository`, `role` | `packages:write` | | |
| 493 | + | | [`remove_actions_access`](/reference/api/packages/remove-package-actions-access/) | Stop a `repository`'s workflows using it. | `repository` | `packages:write` | | |
| 494 | + | | [`delete`](/reference/api/packages/delete-package/) | Delete it, restorable for 30 days; its name stays taken until then. | `package_type`, `package_name` | `packages:delete` | | |
| 495 | + | | [`restore`](/reference/api/packages/restore-package/) | Restore a deleted package. | `package_type`, `package_name` | `packages:delete` | | |
| 496 | + | | [`delete_version`](/reference/api/packages/delete-package-version/) | Delete a version, restorable for 30 days. | `version_id` | `packages:delete` | | |
| 497 | + | | [`restore_version`](/reference/api/packages/restore-package-version/) | Restore a deleted version. | `version_id` | `packages:delete` | | |
| 498 | + | ||
| 469 | 499 | ## `secret` | |
| 470 | 500 | ||
| 471 | 501 | A repository's or a workspace's secrets and variables, which workflows and |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.