Packages pages: Maven, NuGet and RubyGems, routed to the packages service
/-/maven/, /-/nuget/ and /-/rubygems/ go to the packages service as /-/cargo/ does. Each registry has its label, its card on the Packages page with its start command and guide, its install commands on a package's page (a Maven version to fetch, the feed URL for dotnet add package, Bundler's source credentials) and its tint and icon.
7 files+153−50/7 viewed
| 1 | − | import { Container, Layers, Package } from "lucide-react"; | |
| 1 | + | import { Boxes, Container, Feather, Gem, Layers, Package } from "lucide-react"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Ecosystem } from "@g1t/contracts"; | |
| 4 | 4 | ||
| 11 | 11 | composer: "#c08a5b", | |
| 12 | 12 | go: "#00add8", | |
| 13 | 13 | cargo: "#dea584", | |
| 14 | + | maven: "#e8762b", | |
| 15 | + | nuget: "#3d8fd6", | |
| 16 | + | rubygems: "#e9573f", | |
| 14 | 17 | }; | |
| 15 | 18 | ||
| 16 | 19 | /** A small tile marking which registry a package is in. */ | |
| 26 | 29 | {ecosystem === "container" && <Container size={glyph} />} | |
| 27 | 30 | {ecosystem === "composer" && <Layers size={glyph} />} | |
| 28 | 31 | {ecosystem === "cargo" && <Package size={glyph} />} | |
| 32 | + | {ecosystem === "maven" && <Feather size={glyph} />} | |
| 33 | + | {ecosystem === "nuget" && <Boxes size={glyph} />} | |
| 34 | + | {ecosystem === "rubygems" && <Gem size={glyph} />} | |
| 29 | 35 | {ecosystem === "npm" && <span style={{ fontSize: glyph }}>n</span>} | |
| 30 | 36 | {ecosystem === "go" && <span style={{ fontSize: Math.round(size * 0.38) }}>GO</span>} | |
| 31 | 37 | </span> |
| 45 | 45 | assert.equal(installCommands(pkg, null, "ada").install, "cargo add http-client --registry acme"); | |
| 46 | 46 | }); | |
| 47 | 47 | ||
| 48 | + | test("a Maven artifact is fetched from its workspace's repository, with a settings.xml server for private ones", () => { | |
| 49 | + | const pkg = { ecosystem: "maven" as const, address: "g1t.sh/-/maven/acme/com.acme:web", name: "com.acme:web", workspace: "acme" }; | |
| 50 | + | assert.deepEqual(installCommands(pkg, "1.2.0", "ada"), { | |
| 51 | + | login: | |
| 52 | + | "mkdir -p ~/.m2 && [ ! -e ~/.m2/settings.xml ] && printf '<settings><servers><server><id>acme</id><username>ada</username><password>YOUR_TOKEN</password></server></servers></settings>\\n' > ~/.m2/settings.xml", | |
| 53 | + | install: "mvn dependency:get -Dartifact=com.acme:web:1.2.0 -DremoteRepositories=acme::default::https://g1t.sh/-/maven/acme/", | |
| 54 | + | }); | |
| 55 | + | assert.match(installCommands(pkg, null, "ada").install, /-Dartifact=com\.acme:web:LATEST /); | |
| 56 | + | }); | |
| 57 | + | ||
| 58 | + | test("a NuGet package is added from its workspace's feed, named as a source with credentials for private ones", () => { | |
| 59 | + | const pkg = { ecosystem: "nuget" as const, address: "g1t.sh/-/nuget/acme/Acme.Web", name: "Acme.Web", workspace: "acme" }; | |
| 60 | + | assert.deepEqual(installCommands(pkg, "1.0.0", "ada"), { | |
| 61 | + | registry: "dotnet nuget add source https://g1t.sh/-/nuget/acme/v3/index.json --name acme", | |
| 62 | + | login: "dotnet nuget update source acme --username ada --password YOUR_TOKEN --store-password-in-clear-text", | |
| 63 | + | install: "dotnet add package Acme.Web --version 1.0.0 --source https://g1t.sh/-/nuget/acme/v3/index.json", | |
| 64 | + | }); | |
| 65 | + | assert.equal(installCommands(pkg, null, "ada").install, "dotnet add package Acme.Web --source https://g1t.sh/-/nuget/acme/v3/index.json"); | |
| 66 | + | }); | |
| 67 | + | ||
| 68 | + | test("a gem is added with Bundler from its workspace's registry, with credentials for private ones", () => { | |
| 69 | + | const pkg = { ecosystem: "rubygems" as const, address: "g1t.sh/-/rubygems/acme/hello", name: "hello", workspace: "acme" }; | |
| 70 | + | assert.deepEqual(installCommands(pkg, "0.1.0", "ada"), { | |
| 71 | + | login: "bundle config set --global https://g1t.sh/-/rubygems/acme/ ada:YOUR_TOKEN", | |
| 72 | + | install: "bundle add hello --version 0.1.0 --source https://g1t.sh/-/rubygems/acme/", | |
| 73 | + | }); | |
| 74 | + | assert.equal(installCommands(pkg, null, "ada").install, "bundle add hello --source https://g1t.sh/-/rubygems/acme/"); | |
| 75 | + | }); | |
| 76 | + | ||
| 48 | 77 | test("a container image is pulled by its address and tag", () => { | |
| 49 | 78 | const pkg = { ecosystem: "container" as const, address: "g1t.sh/acme/web", name: "web", workspace: "acme" }; | |
| 50 | 79 | assert.deepEqual(installCommands(pkg, "latest", "ada"), { |
| 11 | 11 | composer: "Composer", | |
| 12 | 12 | cargo: "Cargo", | |
| 13 | 13 | go: "Go", | |
| 14 | + | maven: "Maven", | |
| 15 | + | nuget: "NuGet", | |
| 16 | + | rubygems: "RubyGems", | |
| 14 | 17 | }; | |
| 15 | 18 | ||
| 16 | 19 | /** | |
| 53 | 56 | guide: "/guides/go/", | |
| 54 | 57 | ready: true, | |
| 55 | 58 | }, | |
| 59 | + | { | |
| 60 | + | ecosystem: "maven", | |
| 61 | + | blurb: "Java and Kotlin libraries in a Maven repository of the workspace's own, deployed with Maven or Gradle, SNAPSHOTs included.", | |
| 62 | + | start: "mvn deploy", | |
| 63 | + | guide: "/guides/maven/", | |
| 64 | + | ready: true, | |
| 65 | + | }, | |
| 66 | + | { | |
| 67 | + | ecosystem: "nuget", | |
| 68 | + | blurb: ".NET packages in a NuGet feed of the workspace's own, pushed with dotnet nuget push and restored with dotnet.", | |
| 69 | + | start: "dotnet nuget push <package>.nupkg --source <workspace>", | |
| 70 | + | guide: "/guides/nuget/", | |
| 71 | + | ready: true, | |
| 72 | + | }, | |
| 73 | + | { | |
| 74 | + | ecosystem: "rubygems", | |
| 75 | + | blurb: "Ruby gems in a registry of the workspace's own, pushed with gem push and installed with Bundler.", | |
| 76 | + | start: "gem push <gem>.gem --host https://g1t.sh/-/rubygems/<workspace>", | |
| 77 | + | guide: "/guides/rubygems/", | |
| 78 | + | ready: true, | |
| 79 | + | }, | |
| 56 | 80 | ]; | |
| 57 | 81 | ||
| 58 | 82 | export type PackageSort = "updated" | "downloads" | "name"; | |
| 156 | 180 | login: `go env -w GOPRIVATE=${host}/${pkg.workspace}`, | |
| 157 | 181 | install: `go get ${pkg.address}${version ? `@${version}` : ""}`, | |
| 158 | 182 | }; | |
| 183 | + | case "maven": { | |
| 184 | + | // Maven takes the repository with the artifact; its credentials are a | |
| 185 | + | // <server> of the same id in ~/.m2/settings.xml, written here only | |
| 186 | + | // when there is no settings.xml yet. | |
| 187 | + | const repository = `https://${host}/-/maven/${pkg.workspace}/`; | |
| 188 | + | return { | |
| 189 | + | login: `mkdir -p ~/.m2 && [ ! -e ~/.m2/settings.xml ] && printf '<settings><servers><server><id>${pkg.workspace}</id><username>${you}</username><password>YOUR_TOKEN</password></server></servers></settings>\\n' > ~/.m2/settings.xml`, | |
| 190 | + | install: `mvn dependency:get -Dartifact=${pkg.name}${version ? `:${version}` : ":LATEST"} -DremoteRepositories=${pkg.workspace}::default::${repository}`, | |
| 191 | + | }; | |
| 192 | + | } | |
| 193 | + | case "nuget": | |
| 194 | + | // The workspace's feed as a named source (needed for any install), | |
| 195 | + | // then the credentials a private package also needs. | |
| 196 | + | return { | |
| 197 | + | registry: `dotnet nuget add source https://${host}/-/nuget/${pkg.workspace}/v3/index.json --name ${pkg.workspace}`, | |
| 198 | + | login: `dotnet nuget update source ${pkg.workspace} --username ${you} --password YOUR_TOKEN --store-password-in-clear-text`, | |
| 199 | + | install: `dotnet add package ${pkg.name}${version ? ` --version ${version}` : ""} --source https://${host}/-/nuget/${pkg.workspace}/v3/index.json`, | |
| 200 | + | }; | |
| 201 | + | case "rubygems": { | |
| 202 | + | const source = `https://${host}/-/rubygems/${pkg.workspace}/`; | |
| 203 | + | return { | |
| 204 | + | login: `bundle config set --global ${source} ${you}:YOUR_TOKEN`, | |
| 205 | + | install: `bundle add ${pkg.name}${version ? ` --version ${version}` : ""} --source ${source}`, | |
| 206 | + | }; | |
| 207 | + | } | |
| 159 | 208 | } | |
| 160 | 209 | } |
| 7 | 7 | import type { DeploysSetting, Ecosystem, PackageSummary, ProjectEcosystem } from "@g1t/contracts"; | |
| 8 | 8 | ||
| 9 | 9 | /** As lib/packages.ts names each registry; repeated so this module stands alone in tests. */ | |
| 10 | − | const REGISTRY: Record<Ecosystem, string> = { container: "Container", npm: "npm", composer: "Composer", cargo: "Cargo", go: "Go" }; | |
| 10 | + | const REGISTRY: Record<Ecosystem, string> = { | |
| 11 | + | container: "Container", | |
| 12 | + | npm: "npm", | |
| 13 | + | composer: "Composer", | |
| 14 | + | cargo: "Cargo", | |
| 15 | + | go: "Go", | |
| 16 | + | maven: "Maven", | |
| 17 | + | nuget: "NuGet", | |
| 18 | + | rubygems: "RubyGems", | |
| 19 | + | }; | |
| 11 | 20 | ||
| 12 | 21 | const DOCS = "https://docs.g1t.sh"; | |
| 13 | 22 |
| 64 | 64 | assert.equal(servicePath("/acme/-/cargo/x"), null); | |
| 65 | 65 | }); | |
| 66 | 66 | ||
| 67 | + | test("the Maven repositories go to the packages service", () => { | |
| 68 | + | for (const path of [ | |
| 69 | + | "/-/maven/acme/com/acme/web/maven-metadata.xml", | |
| 70 | + | "/-/maven/acme/com/acme/web/maven-metadata.xml.sha1", | |
| 71 | + | "/-/maven/acme/com/acme/web/1.0.0/web-1.0.0.jar", | |
| 72 | + | "/-/maven/acme/com/acme/web/1.0-SNAPSHOT/web-1.0-20261006.120000-1.pom", | |
| 73 | + | // An artifact named like a git endpoint is still Maven's. | |
| 74 | + | "/-/maven/acme/com/acme/info/refs", | |
| 75 | + | ]) { | |
| 76 | + | assert.equal(servicePath(path), "packages", path); | |
| 77 | + | } | |
| 78 | + | assert.equal(servicePath("/-/maven"), null); | |
| 79 | + | assert.equal(servicePath("/acme/-/maven/x"), null); | |
| 80 | + | }); | |
| 81 | + | ||
| 82 | + | test("the NuGet feeds go to the packages service", () => { | |
| 83 | + | for (const path of [ | |
| 84 | + | "/-/nuget/acme/v3/index.json", | |
| 85 | + | "/-/nuget/acme/v3/flatcontainer/acme.web/index.json", | |
| 86 | + | "/-/nuget/acme/v3/flatcontainer/acme.web/1.0.0/acme.web.1.0.0.nupkg", | |
| 87 | + | "/-/nuget/acme/v3/registration/acme.web/index.json", | |
| 88 | + | "/-/nuget/acme/v3/query", | |
| 89 | + | "/-/nuget/acme/api/v2/package", | |
| 90 | + | "/-/nuget/acme/api/v2/package/Acme.Web/1.0.0", | |
| 91 | + | ]) { | |
| 92 | + | assert.equal(servicePath(path), "packages", path); | |
| 93 | + | } | |
| 94 | + | assert.equal(servicePath("/-/nuget"), null); | |
| 95 | + | assert.equal(servicePath("/acme/-/nuget/x"), null); | |
| 96 | + | }); | |
| 97 | + | ||
| 98 | + | test("the RubyGems registries go to the packages service", () => { | |
| 99 | + | for (const path of [ | |
| 100 | + | "/-/rubygems/acme/versions", | |
| 101 | + | "/-/rubygems/acme/info/hello", | |
| 102 | + | "/-/rubygems/acme/names", | |
| 103 | + | "/-/rubygems/acme/gems/hello-0.1.0.gem", | |
| 104 | + | "/-/rubygems/acme/api/v1/gems", | |
| 105 | + | "/-/rubygems/acme/api/v1/gems/yank", | |
| 106 | + | ]) { | |
| 107 | + | assert.equal(servicePath(path), "packages", path); | |
| 108 | + | } | |
| 109 | + | assert.equal(servicePath("/-/rubygems"), null); | |
| 110 | + | assert.equal(servicePath("/acme/-/rubygems/x"), null); | |
| 111 | + | }); | |
| 112 | + | ||
| 67 | 113 | test("git goes to repos, and everything else is the site's", () => { | |
| 68 | 114 | assert.equal(servicePath("/acme/web.git/info/refs"), "git"); | |
| 69 | 115 | assert.equal(servicePath("/acme/web/git-receive-pack"), "git"); |
| 13 | 13 | const COMPOSER_PATH = /^\/-\/composer\//; | |
| 14 | 14 | /** The Cargo registries: `/-/cargo/<workspace>/`, a sparse index and its web API, one per workspace. */ | |
| 15 | 15 | const CARGO_PATH = /^\/-\/cargo\//; | |
| 16 | + | /** The Maven repositories: `/-/maven/<workspace>/`, in the standard layout, one per workspace. */ | |
| 17 | + | const MAVEN_PATH = /^\/-\/maven\//; | |
| 18 | + | /** The NuGet feeds: `/-/nuget/<workspace>/v3/index.json` and what it names, one per workspace. */ | |
| 19 | + | const NUGET_PATH = /^\/-\/nuget\//; | |
| 20 | + | /** The RubyGems registries: `/-/rubygems/<workspace>/`, the compact index and `gem push`, one per workspace. */ | |
| 21 | + | const RUBYGEMS_PATH = /^\/-\/rubygems\//; | |
| 22 | + | ||
| 23 | + | const REGISTRY_PATHS = [REGISTRY_PATH, NPM_PATH, COMPOSER_PATH, CARGO_PATH, MAVEN_PATH, NUGET_PATH, RUBYGEMS_PATH]; | |
| 16 | 24 | ||
| 17 | 25 | export type ServicePath = "git" | "packages" | null; | |
| 18 | 26 | ||
| 19 | 27 | export function servicePath(pathname: string): ServicePath { | |
| 20 | − | if (REGISTRY_PATH.test(pathname) || NPM_PATH.test(pathname) || COMPOSER_PATH.test(pathname) || CARGO_PATH.test(pathname)) return "packages"; | |
| 28 | + | if (REGISTRY_PATHS.some((path) => path.test(pathname))) return "packages"; | |
| 21 | 29 | if (GIT_PATH.test(pathname)) return "git"; | |
| 22 | 30 | return null; | |
| 23 | 31 | } |
| 72 | 72 | const { detail, username } = loaderData; | |
| 73 | 73 | const { package: pkg, versions, tags, permissions } = detail; | |
| 74 | 74 | const outcome = actionData as Outcome | undefined; | |
| 75 | − | const latest = tags.find((tag) => tag.tag === "latest")?.tag ?? tags[0]?.tag ?? null; | |
| 75 | + | // Maven names a version to fetch; the others install their newest without one. | |
| 76 | + | const latest = tags.find((tag) => tag.tag === "latest")?.tag ?? tags[0]?.tag ?? (pkg.ecosystem === "maven" ? pkg.latest : null); | |
| 76 | 77 | const commands = installCommands(pkg, latest, username); | |
| 77 | 78 | // npm and Composer versions are numbers; images are digests and tags. | |
| 78 | 79 | const npm = pkg.ecosystem !== "container"; | |
| 130 | 131 | with <code className="font-mono">packages:read</code>. | |
| 131 | 132 | </p> | |
| 132 | 133 | )} | |
| 133 | − | {commands.registry && pkg.visibility === "private" && pkg.ecosystem !== "cargo" && ( | |
| 134 | + | {pkg.visibility === "private" && commands.login.includes("YOUR_TOKEN") && ( | |
| 134 | 135 | <p className="text-xs text-faint"> | |
| 135 | 136 | Put an{" "} | |
| 136 | 137 | <Link to="/settings/tokens" className="text-muted hover:text-fg"> |