Docs: Maven, NuGet and RubyGems guides
A guide for each registry, in the sidebar and in the Packages guide's table: credentials, publishing, installing, version rules, private and public packages, workflows with G1T_TOKEN, size and errors.
5 files+641−60/5 viewed
| 78 | 78 | { label: 'npm', slug: 'guides/npm' }, | |
| 79 | 79 | { label: 'Cargo', slug: 'guides/cargo' }, | |
| 80 | 80 | { label: 'Composer', slug: 'guides/composer' }, | |
| 81 | + | { label: 'Maven', slug: 'guides/maven' }, | |
| 82 | + | { label: 'NuGet', slug: 'guides/nuget' }, | |
| 83 | + | { label: 'RubyGems', slug: 'guides/rubygems' }, | |
| 81 | 84 | { label: 'Go modules', slug: 'guides/go' }, | |
| 82 | 85 | { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' }, | |
| 83 | 86 | { label: 'Security', slug: 'guides/security' }, |
| 1 | + | --- | |
| 2 | + | title: Maven | |
| 3 | + | description: Deploy and depend on a workspace's Java and Kotlin libraries with Maven or Gradle, from a Maven repository of its own on g1t.sh, SNAPSHOTs included. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | Every workspace has a Maven repository of its own, in the standard | |
| 7 | + | layout that Maven, Gradle and every other JVM build tool read. `mvn | |
| 8 | + | deploy` and Gradle's `publish` upload to it, and builds resolve | |
| 9 | + | dependencies from it, private ones included. | |
| 10 | + | ||
| 11 | + | ```text | |
| 12 | + | https://g1t.sh/-/maven/<workspace>/ | |
| 13 | + | ``` | |
| 14 | + | ||
| 15 | + | Dependencies from Maven Central still come from Maven Central; only the | |
| 16 | + | artifacts you deploy here come from g1t. | |
| 17 | + | ||
| 18 | + | ## Credentials | |
| 19 | + | ||
| 20 | + | Maven and Gradle send Basic credentials: any username, and an | |
| 21 | + | [access token](https://g1t.sh/settings/tokens) as the password. A token | |
| 22 | + | with full access works; one with scopes needs `packages:read` to resolve | |
| 23 | + | private artifacts and `packages:write` to deploy. Public artifacts resolve | |
| 24 | + | without one. | |
| 25 | + | ||
| 26 | + | For Maven, put the credentials in `~/.m2/settings.xml`, as a `<server>` | |
| 27 | + | whose `id` is the one you give the repository in `pom.xml`. These pages use | |
| 28 | + | the workspace's slug: | |
| 29 | + | ||
| 30 | + | ```xml | |
| 31 | + | <settings> | |
| 32 | + | <servers> | |
| 33 | + | <server> | |
| 34 | + | <id>acme</id> | |
| 35 | + | <username>ada</username> | |
| 36 | + | <password>${env.G1T_TOKEN}</password> | |
| 37 | + | </server> | |
| 38 | + | </servers> | |
| 39 | + | </settings> | |
| 40 | + | ``` | |
| 41 | + | ||
| 42 | + | Maven reads `${env.G1T_TOKEN}` from the environment, so the token stays | |
| 43 | + | out of the file. | |
| 44 | + | ||
| 45 | + | For Gradle, keep them in `~/.gradle/gradle.properties`, named for the | |
| 46 | + | repository (`acmeUsername` and `acmePassword` for a repository named | |
| 47 | + | `acme`), or read them from the environment as the examples below do. | |
| 48 | + | ||
| 49 | + | ## Deploy with Maven | |
| 50 | + | ||
| 51 | + | Name the repository in `pom.xml`, for releases and SNAPSHOTs alike, and say | |
| 52 | + | which repository the source is in: | |
| 53 | + | ||
| 54 | + | ```xml | |
| 55 | + | <project> | |
| 56 | + | <groupId>com.acme</groupId> | |
| 57 | + | <artifactId>http-client</artifactId> | |
| 58 | + | <version>0.3.1</version> | |
| 59 | + | ||
| 60 | + | <scm> | |
| 61 | + | <url>https://g1t.sh/acme/http-client</url> | |
| 62 | + | </scm> | |
| 63 | + | ||
| 64 | + | <distributionManagement> | |
| 65 | + | <repository> | |
| 66 | + | <id>acme</id> | |
| 67 | + | <url>https://g1t.sh/-/maven/acme/</url> | |
| 68 | + | </repository> | |
| 69 | + | <snapshotRepository> | |
| 70 | + | <id>acme</id> | |
| 71 | + | <url>https://g1t.sh/-/maven/acme/</url> | |
| 72 | + | </snapshotRepository> | |
| 73 | + | </distributionManagement> | |
| 74 | + | </project> | |
| 75 | + | ``` | |
| 76 | + | ||
| 77 | + | ```sh | |
| 78 | + | mvn deploy | |
| 79 | + | ``` | |
| 80 | + | ||
| 81 | + | Maven uploads the artifact, its POM and their checksums, then the | |
| 82 | + | artifact's `maven-metadata.xml`. The artifact is named by its coordinates, | |
| 83 | + | `com.acme:http-client`, and its page on g1t.sh shows the description from | |
| 84 | + | the POM of its highest release. | |
| 85 | + | ||
| 86 | + | ## Deploy with Gradle | |
| 87 | + | ||
| 88 | + | With the `maven-publish` plugin, in `build.gradle.kts`: | |
| 89 | + | ||
| 90 | + | ```kotlin | |
| 91 | + | plugins { | |
| 92 | + | `java-library` | |
| 93 | + | `maven-publish` | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | group = "com.acme" | |
| 97 | + | version = "0.3.1" | |
| 98 | + | ||
| 99 | + | publishing { | |
| 100 | + | publications { | |
| 101 | + | create<MavenPublication>("library") { | |
| 102 | + | from(components["java"]) | |
| 103 | + | pom { | |
| 104 | + | scm { url = "https://g1t.sh/acme/http-client" } | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | repositories { | |
| 109 | + | maven { | |
| 110 | + | name = "acme" | |
| 111 | + | url = uri("https://g1t.sh/-/maven/acme/") | |
| 112 | + | credentials(PasswordCredentials::class) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ``` | |
| 117 | + | ||
| 118 | + | ```sh | |
| 119 | + | ./gradlew publish | |
| 120 | + | ``` | |
| 121 | + | ||
| 122 | + | `credentials(PasswordCredentials::class)` reads `acmeUsername` and | |
| 123 | + | `acmePassword` from `gradle.properties` or from the environment as | |
| 124 | + | `ORG_GRADLE_PROJECT_acmeUsername` and `ORG_GRADLE_PROJECT_acmePassword`. | |
| 125 | + | Gradle's Gradle Module Metadata (`.module`) is uploaded and served beside | |
| 126 | + | the POM. | |
| 127 | + | ||
| 128 | + | ## Which repository an artifact belongs to | |
| 129 | + | ||
| 130 | + | The first file deployed makes the artifact. When a repository of the | |
| 131 | + | workspace is named like its artifactId (`http-client`), it is linked to | |
| 132 | + | that repository and has its visibility and roles: deploying needs Write | |
| 133 | + | on it. Otherwise, the POM's `<scm><url>` (or its `<url>`) naming a g1t.sh | |
| 134 | + | repository of the workspace links a new artifact to that repository, if you | |
| 135 | + | may write to it. An artifact linked to neither is the workspace's, private, | |
| 136 | + | and needs the workspace's Write base permission. See | |
| 137 | + | [who can see and publish a package](/guides/packages/#who-can-see-and-publish-a-package). | |
| 138 | + | ||
| 139 | + | ## Depend on an artifact | |
| 140 | + | ||
| 141 | + | In `pom.xml`, name the repository, with the same `id` as the `<server>` | |
| 142 | + | holding your credentials: | |
| 143 | + | ||
| 144 | + | ```xml | |
| 145 | + | <repositories> | |
| 146 | + | <repository> | |
| 147 | + | <id>acme</id> | |
| 148 | + | <url>https://g1t.sh/-/maven/acme/</url> | |
| 149 | + | </repository> | |
| 150 | + | </repositories> | |
| 151 | + | ||
| 152 | + | <dependencies> | |
| 153 | + | <dependency> | |
| 154 | + | <groupId>com.acme</groupId> | |
| 155 | + | <artifactId>http-client</artifactId> | |
| 156 | + | <version>0.3.1</version> | |
| 157 | + | </dependency> | |
| 158 | + | </dependencies> | |
| 159 | + | ``` | |
| 160 | + | ||
| 161 | + | In Gradle: | |
| 162 | + | ||
| 163 | + | ```kotlin | |
| 164 | + | repositories { | |
| 165 | + | mavenCentral() | |
| 166 | + | maven { | |
| 167 | + | name = "acme" | |
| 168 | + | url = uri("https://g1t.sh/-/maven/acme/") | |
| 169 | + | credentials(PasswordCredentials::class) | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | dependencies { | |
| 174 | + | implementation("com.acme:http-client:0.3.1") | |
| 175 | + | } | |
| 176 | + | ``` | |
| 177 | + | ||
| 178 | + | To fetch one artifact without a project: | |
| 179 | + | ||
| 180 | + | ```sh | |
| 181 | + | mvn dependency:get -Dartifact=com.acme:http-client:0.3.1 \ | |
| 182 | + | -DremoteRepositories=acme::default::https://g1t.sh/-/maven/acme/ | |
| 183 | + | ``` | |
| 184 | + | ||
| 185 | + | ## SNAPSHOTs | |
| 186 | + | ||
| 187 | + | A version ending in `-SNAPSHOT` takes a new build each time it is | |
| 188 | + | deployed. Maven and Gradle upload each build's files with a timestamp and | |
| 189 | + | build number in their names (`http-client-0.4.0-20261006.120000-3.jar`), | |
| 190 | + | and the version's `maven-metadata.xml` names the newest build of each file, | |
| 191 | + | which is what a build depending on `0.4.0-SNAPSHOT` resolves. Earlier | |
| 192 | + | builds stay, by their full names. | |
| 193 | + | ||
| 194 | + | ## Versions and files | |
| 195 | + | ||
| 196 | + | - **A release's files are written once.** Deploying a file of a released | |
| 197 | + | version again with different content is refused with `409`; the same | |
| 198 | + | content again is accepted, so a deploy that stopped part way can be run | |
| 199 | + | again. Bump the version to change a release. | |
| 200 | + | - **Checksums are worked out by g1t.** The `.md5`, `.sha1`, `.sha256` and | |
| 201 | + | `.sha512` beside every file are answered from the file itself. Checksums | |
| 202 | + | uploaded are checked against it, and a mismatch is refused with `400`. | |
| 203 | + | - **`maven-metadata.xml` is made by g1t** from the versions there, so it | |
| 204 | + | always lists every version, with the highest as `latest` and the highest | |
| 205 | + | that is not a SNAPSHOT as `release`. The one a build uploads is accepted | |
| 206 | + | and not kept. | |
| 207 | + | - **The deploy's last step publishes it.** Maven and Gradle upload the | |
| 208 | + | artifact's `maven-metadata.xml` after its files. Then each version (or | |
| 209 | + | SNAPSHOT build) whose POM arrived in the deploy is published: it is an | |
| 210 | + | audit entry and a `package.published` event, with every file in place. | |
| 211 | + | A POM whose `groupId`, `artifactId` or `version` is not the one in its | |
| 212 | + | path is refused with `400`. | |
| 213 | + | ||
| 214 | + | Delete a version, or the artifact, on its page on g1t.sh, with Admin on | |
| 215 | + | the linked repository (an owner, for the workspace's own artifacts). | |
| 216 | + | ||
| 217 | + | ## Private and public artifacts | |
| 218 | + | ||
| 219 | + | An artifact linked to a repository has the repository's visibility; one of | |
| 220 | + | the workspace's own is private until an owner makes it public on its page. | |
| 221 | + | ||
| 222 | + | | The workspace's artifacts | Without credentials | With credentials | | |
| 223 | + | | --- | --- | --- | | |
| 224 | + | | All public | Maven and Gradle download them. | The same; credentials are sent to deploy. | | |
| 225 | + | | Some private | Every request without credentials is answered `401`, which makes Maven and Gradle send theirs. | Each artifact the credentials' owner may see. | | |
| 226 | + | ||
| 227 | + | A private artifact you cannot see looks exactly like one that does not | |
| 228 | + | exist. | |
| 229 | + | ||
| 230 | + | ## In workflows | |
| 231 | + | ||
| 232 | + | A workflow's `G1T_TOKEN` is the workspace's own token for the run, and can | |
| 233 | + | resolve and deploy the workspace's artifacts. With the `settings.xml` | |
| 234 | + | above, give it to Maven as `G1T_TOKEN`: | |
| 235 | + | ||
| 236 | + | ```yaml | |
| 237 | + | jobs: | |
| 238 | + | deploy: | |
| 239 | + | runs-on: ubuntu-latest | |
| 240 | + | env: | |
| 241 | + | G1T_TOKEN: ${{ secrets.G1T_TOKEN }} | |
| 242 | + | steps: | |
| 243 | + | - uses: actions/checkout@v4 | |
| 244 | + | - run: | | |
| 245 | + | mkdir -p ~/.m2 | |
| 246 | + | printf '<settings><servers><server><id>acme</id><username>g1t</username><password>${env.G1T_TOKEN}</password></server></servers></settings>\n' > ~/.m2/settings.xml | |
| 247 | + | - run: mvn --batch-mode deploy | |
| 248 | + | ``` | |
| 249 | + | ||
| 250 | + | For Gradle, set `ORG_GRADLE_PROJECT_acmeUsername: g1t` and | |
| 251 | + | `ORG_GRADLE_PROJECT_acmePassword: ${{ secrets.G1T_TOKEN }}` and run | |
| 252 | + | `./gradlew publish`. | |
| 253 | + | ||
| 254 | + | ## Size | |
| 255 | + | ||
| 256 | + | Each file is one request and may be at most 100 MB. Without the | |
| 257 | + | [g1t plan](/guides/usage-and-billing/#the-g1t-plan), a workspace's private | |
| 258 | + | packages may hold 500 MB and its public ones 10 GB, as for | |
| 259 | + | [container images](/guides/containers/#storage-and-pull-limits). A file is | |
| 260 | + | stored once, by its content. | |
| 261 | + | ||
| 262 | + | ## Errors | |
| 263 | + | ||
| 264 | + | | Error | Means | | |
| 265 | + | | --- | --- | | |
| 266 | + | | `401` | No credentials, or a wrong or expired token. Check the `<server>` whose `id` matches the repository's, or Gradle's `acmeUsername` and `acmePassword`. | | |
| 267 | + | | `403` | Signed in, but your role or your token's scopes do not allow it, or the workspace is out of free package storage. The response says which. | | |
| 268 | + | | `404` | No such artifact, version or file, or a private one you cannot see. | | |
| 269 | + | | `409` | That file of a release is already there with other content. Bump the version. | | |
| 270 | + | | `400` | The upload was refused: a POM that does not match its path, or a checksum that does not match its file. The response says which. | | |
| 271 | + | | `413` | The file is over 100 MB. | |
| 1 | + | --- | |
| 2 | + | title: NuGet | |
| 3 | + | description: Push and restore a workspace's .NET packages with dotnet, from a NuGet feed of its own on g1t.sh, from your machine and from workflows. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | Every workspace has a NuGet feed of its own, speaking the NuGet v3 | |
| 7 | + | protocol that `dotnet`, Visual Studio, Rider and `nuget.exe` read. `dotnet | |
| 8 | + | nuget push` publishes to it, and restores install from it, private | |
| 9 | + | packages included. | |
| 10 | + | ||
| 11 | + | ```text | |
| 12 | + | https://g1t.sh/-/nuget/<workspace>/v3/index.json | |
| 13 | + | ``` | |
| 14 | + | ||
| 15 | + | Packages from nuget.org still come from nuget.org; only the packages you | |
| 16 | + | push here come from g1t. | |
| 17 | + | ||
| 18 | + | ## Add the feed | |
| 19 | + | ||
| 20 | + | Add the feed as a package source, named for the workspace. For a feed with | |
| 21 | + | private packages, give it a username (any works) and an | |
| 22 | + | [access token](https://g1t.sh/settings/tokens) as the password: | |
| 23 | + | ||
| 24 | + | ```sh | |
| 25 | + | dotnet nuget add source https://g1t.sh/-/nuget/acme/v3/index.json --name acme \ | |
| 26 | + | --username ada --password <token> --store-password-in-clear-text | |
| 27 | + | ``` | |
| 28 | + | ||
| 29 | + | That writes the source into your user `NuGet.Config`. To keep it with the | |
| 30 | + | project instead, put a `nuget.config` beside the solution, with the token | |
| 31 | + | read from the environment: | |
| 32 | + | ||
| 33 | + | ```xml | |
| 34 | + | <?xml version="1.0" encoding="utf-8"?> | |
| 35 | + | <configuration> | |
| 36 | + | <packageSources> | |
| 37 | + | <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> | |
| 38 | + | <add key="acme" value="https://g1t.sh/-/nuget/acme/v3/index.json" /> | |
| 39 | + | </packageSources> | |
| 40 | + | <packageSourceCredentials> | |
| 41 | + | <acme> | |
| 42 | + | <add key="Username" value="ada" /> | |
| 43 | + | <add key="ClearTextPassword" value="%G1T_TOKEN%" /> | |
| 44 | + | </acme> | |
| 45 | + | </packageSourceCredentials> | |
| 46 | + | </configuration> | |
| 47 | + | ``` | |
| 48 | + | ||
| 49 | + | A token with full access works; one with scopes needs `packages:read` to | |
| 50 | + | restore private packages and `packages:write` to push and unlist. A feed | |
| 51 | + | whose packages are all public restores without credentials. | |
| 52 | + | ||
| 53 | + | ## Push | |
| 54 | + | ||
| 55 | + | Say in the project file what the package is and which repository it comes | |
| 56 | + | from: | |
| 57 | + | ||
| 58 | + | ```xml | |
| 59 | + | <PropertyGroup> | |
| 60 | + | <PackageId>Acme.Http</PackageId> | |
| 61 | + | <Version>0.3.1</Version> | |
| 62 | + | <Description>Our HTTP client</Description> | |
| 63 | + | <RepositoryUrl>https://g1t.sh/acme/http</RepositoryUrl> | |
| 64 | + | <PackageReadmeFile>README.md</PackageReadmeFile> | |
| 65 | + | </PropertyGroup> | |
| 66 | + | <ItemGroup> | |
| 67 | + | <None Include="README.md" Pack="true" PackagePath="\" /> | |
| 68 | + | </ItemGroup> | |
| 69 | + | ``` | |
| 70 | + | ||
| 71 | + | ```sh | |
| 72 | + | dotnet pack -c Release | |
| 73 | + | dotnet nuget push bin/Release/Acme.Http.0.3.1.nupkg --source acme --api-key <token> | |
| 74 | + | ``` | |
| 75 | + | ||
| 76 | + | The token is the API key. The first push makes the package. When its | |
| 77 | + | `RepositoryUrl` is a g1t.sh repository of the same workspace, or a | |
| 78 | + | repository is named like its id in lowercase (`acme.http`, or `acme-http`), | |
| 79 | + | it is linked to that repository and has its visibility and roles: pushing | |
| 80 | + | needs Write on it. Otherwise it is the workspace's, private, and needs the | |
| 81 | + | workspace's Write base permission. See | |
| 82 | + | [who can see and publish a package](/guides/packages/#who-can-see-and-publish-a-package). | |
| 83 | + | ||
| 84 | + | The package's page on g1t.sh shows the README the package names | |
| 85 | + | (`PackageReadmeFile`) and the description of its highest stable version. | |
| 86 | + | ||
| 87 | + | ## Restore | |
| 88 | + | ||
| 89 | + | ```sh | |
| 90 | + | dotnet add package Acme.Http --source https://g1t.sh/-/nuget/acme/v3/index.json | |
| 91 | + | ``` | |
| 92 | + | ||
| 93 | + | `dotnet add package` takes the feed's address here, not the source's | |
| 94 | + | name; the credentials still come from the source of that address. After | |
| 95 | + | that, `dotnet restore` and `dotnet build` find the package through the | |
| 96 | + | source. If your `nuget.config` uses | |
| 97 | + | [package source mapping](https://learn.microsoft.com/nuget/consume-packages/package-source-mapping), | |
| 98 | + | map the workspace's packages to the feed: | |
| 99 | + | ||
| 100 | + | ```xml | |
| 101 | + | <packageSourceMapping> | |
| 102 | + | <packageSource key="nuget.org"><package pattern="*" /></packageSource> | |
| 103 | + | <packageSource key="acme"><package pattern="Acme.*" /></packageSource> | |
| 104 | + | </packageSourceMapping> | |
| 105 | + | ``` | |
| 106 | + | ||
| 107 | + | `dotnet package search Acme --source acme` lists the workspace's packages | |
| 108 | + | you can see whose ids or descriptions match. | |
| 109 | + | ||
| 110 | + | ## Ids and versions | |
| 111 | + | ||
| 112 | + | An id is letters, digits and `_`, in parts joined by `.` or `-`, at most | |
| 113 | + | 100 characters. Ids are one whatever their case: once `Acme.Http` is | |
| 114 | + | pushed, `acme.http` is the same package. | |
| 115 | + | ||
| 116 | + | Versions are read as NuGet reads them: `1.0` is `1.0.0`, and build metadata | |
| 117 | + | (`+abc`) is not part of the version. A version is pushed once: pushing | |
| 118 | + | one that is already there, listed or not, is refused with `409`, so bump | |
| 119 | + | `Version` first. | |
| 120 | + | ||
| 121 | + | ## Unlist | |
| 122 | + | ||
| 123 | + | ```sh | |
| 124 | + | dotnet nuget delete Acme.Http 0.3.1 --source acme --api-key <token> --non-interactive | |
| 125 | + | ``` | |
| 126 | + | ||
| 127 | + | As on nuget.org, this unlists the version rather than deleting it: | |
| 128 | + | projects that name it still restore it, but search no longer shows it, and | |
| 129 | + | its registration says `"listed": false`. Unlisting needs | |
| 130 | + | what pushing does. The package's page marks unlisted versions, and someone | |
| 131 | + | with Admin on the linked repository (an owner, for the workspace's own | |
| 132 | + | packages) can delete a version there for good. | |
| 133 | + | ||
| 134 | + | ## Private and public packages | |
| 135 | + | ||
| 136 | + | A package linked to a repository has the repository's visibility; one of | |
| 137 | + | the workspace's own is private until an owner makes it public on its page. | |
| 138 | + | ||
| 139 | + | | The workspace's packages | Without credentials | With credentials | | |
| 140 | + | | --- | --- | --- | | |
| 141 | + | | All public | `dotnet` reads the feed and restores them. | The same; the API key is sent to push. | | |
| 142 | + | | Some private | The feed answers `401`, which makes `dotnet` send the source's credentials. | Each package the credentials' owner may see. | | |
| 143 | + | ||
| 144 | + | A private package you cannot see looks exactly like one that does not | |
| 145 | + | exist. | |
| 146 | + | ||
| 147 | + | ## In workflows | |
| 148 | + | ||
| 149 | + | A workflow's `G1T_TOKEN` is the workspace's own token for the run, and can | |
| 150 | + | restore and push the workspace's packages. With the `nuget.config` above, | |
| 151 | + | which reads `%G1T_TOKEN%`: | |
| 152 | + | ||
| 153 | + | ```yaml | |
| 154 | + | jobs: | |
| 155 | + | publish: | |
| 156 | + | runs-on: ubuntu-latest | |
| 157 | + | env: | |
| 158 | + | G1T_TOKEN: ${{ secrets.G1T_TOKEN }} | |
| 159 | + | steps: | |
| 160 | + | - uses: actions/checkout@v4 | |
| 161 | + | - run: dotnet test | |
| 162 | + | - run: dotnet pack -c Release -o out | |
| 163 | + | - run: dotnet nuget push "out/*.nupkg" --source acme --api-key "$G1T_TOKEN" | |
| 164 | + | ``` | |
| 165 | + | ||
| 166 | + | ## Size | |
| 167 | + | ||
| 168 | + | A push is one request with the `.nupkg` inside it, and may hold at most | |
| 169 | + | 100 MB. Without the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), a | |
| 170 | + | workspace's private packages may hold 500 MB and its public ones 10 GB, as | |
| 171 | + | for [container images](/guides/containers/#storage-and-pull-limits). A | |
| 172 | + | `.nupkg` is stored once, by its content. | |
| 173 | + | ||
| 174 | + | ## Errors | |
| 175 | + | ||
| 176 | + | | Error | Means | | |
| 177 | + | | --- | --- | | |
| 178 | + | | `401` | No credentials or API key, or a wrong or expired token. Check the source's username and password, or the `--api-key`. | | |
| 179 | + | | `403` | Signed in, but your role or your token's scopes do not allow it, or the workspace is out of free package storage. The response says which. | | |
| 180 | + | | `404` | No such package or version, or a private one you cannot see. | | |
| 181 | + | | `409` | That version is already pushed. Bump `Version`. | | |
| 182 | + | | `400` | The push was refused: not a `.nupkg`, no `.nuspec` in it, or an id or version NuGet would not take. The response says which. | | |
| 183 | + | | `413` | The `.nupkg` is over 100 MB. | |
| 5 | 5 | ||
| 6 | 6 | A workspace can publish packages to g1t and install them from it, beside | |
| 7 | 7 | the code they are built from: container images, npm packages, Rust | |
| 8 | − | crates, Composer packages and Go modules. Each registry speaks its | |
| 9 | − | tool's own protocol, so `docker`, `npm`, `cargo`, `composer` and `go` | |
| 10 | − | work with nothing but a login and an address. Composer packages and Go modules are | |
| 8 | + | crates, Maven artifacts, NuGet packages, Ruby gems, Composer packages and | |
| 9 | + | Go modules. Each registry speaks its tool's own protocol, so `docker`, | |
| 10 | + | `npm`, `cargo`, `mvn` and Gradle, `dotnet`, `gem` and Bundler, `composer` | |
| 11 | + | and `go` work with nothing but a login and an address. Composer packages and Go modules are | |
| 11 | 12 | read from the workspace's repositories: there is nothing to upload. | |
| 12 | 13 | ||
| 13 | 14 | | Registry | Address | Guide | | |
| 15 | 16 | | Container images | `g1t.sh/<workspace>/<name>` | [Container images](/guides/containers/) | | |
| 16 | 17 | | npm | `https://g1t.sh/-/npm/`, for the scope `@<workspace>` | [npm](/guides/npm/) | | |
| 17 | 18 | | Cargo | `sparse+https://g1t.sh/-/cargo/<workspace>/index/`, a registry per workspace | [Cargo](/guides/cargo/) | | |
| 19 | + | | Maven | `https://g1t.sh/-/maven/<workspace>/`, a repository per workspace, for Maven and Gradle | [Maven](/guides/maven/) | | |
| 20 | + | | NuGet | `https://g1t.sh/-/nuget/<workspace>/v3/index.json`, a feed per workspace | [NuGet](/guides/nuget/) | | |
| 21 | + | | RubyGems | `https://g1t.sh/-/rubygems/<workspace>/`, a registry per workspace, for `gem push` and Bundler | [RubyGems](/guides/rubygems/) | | |
| 18 | 22 | | Composer | `https://g1t.sh/-/composer/<workspace>/`, from the workspace's repositories | [Composer](/guides/composer/) | | |
| 19 | 23 | | Go | `g1t.sh/<workspace>/<repo>`, straight from git | [Go modules](/guides/go/) | | |
| 20 | 24 | ||
| 34 | 38 | an npm package whose `package.json` `repository` is a g1t.sh repository | |
| 35 | 39 | of the workspace, or which is named like one (`@acme/web`), and of a | |
| 36 | 40 | crate whose `Cargo.toml` `repository` is one, or which is named like | |
| 37 | − | one. It then has the repository's visibility and [roles](/guides/access-and-roles/): | |
| 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/): | |
| 38 | 44 | ||
| 39 | 45 | | | Needs | | |
| 40 | 46 | | --- | --- | | |
| 94 | 100 | ## Events and the audit log | |
| 95 | 101 | ||
| 96 | 102 | Publishing a version, deleting a version and deleting a package are | |
| 97 | − | [audit log](/guides/audit-log/) entries (so are deprecating an npm version | |
| 98 | − | and yanking or unyanking a crate version), and the events | |
| 103 | + | [audit log](/guides/audit-log/) entries (so are deprecating an npm version, | |
| 104 | + | yanking or unyanking a crate version, unlisting or listing a NuGet version | |
| 105 | + | and yanking a gem version), and the events | |
| 99 | 106 | `package.published`, `package.version_deleted`, `package.deleted` and | |
| 100 | 107 | `package.visibility_changed`, which [webhooks](/guides/webhooks/) can be | |
| 101 | 108 | sent: a linked package's go to its repository's webhooks and its |
| 1 | + | --- | |
| 2 | + | title: RubyGems | |
| 3 | + | description: Push a workspace's Ruby gems with gem push and install them with Bundler, from a gem registry of its own on g1t.sh, from your machine and from workflows. | |
| 4 | + | --- | |
| 5 | + | ||
| 6 | + | Every workspace has a gem registry of its own. `gem push` publishes to it, | |
| 7 | + | and Bundler installs from it through the compact index (`versions`, | |
| 8 | + | `info/<gem>`), private gems included. Install with Bundler: the registry | |
| 9 | + | serves the compact index, not the older full index (`specs.4.8.gz`) that | |
| 10 | + | `gem install --source` reads. | |
| 11 | + | ||
| 12 | + | ```text | |
| 13 | + | https://g1t.sh/-/rubygems/<workspace>/ | |
| 14 | + | ``` | |
| 15 | + | ||
| 16 | + | Gems from rubygems.org still come from rubygems.org; only the gems you | |
| 17 | + | name with the workspace's source come from g1t. | |
| 18 | + | ||
| 19 | + | ## Push | |
| 20 | + | ||
| 21 | + | Say in the gemspec which repository the gem comes from: | |
| 22 | + | ||
| 23 | + | ```ruby | |
| 24 | + | Gem::Specification.new do |spec| | |
| 25 | + | spec.name = "http-client" | |
| 26 | + | spec.version = "0.3.1" | |
| 27 | + | spec.summary = "Our HTTP client" | |
| 28 | + | spec.authors = ["Ada"] | |
| 29 | + | spec.files = Dir["lib/**/*.rb"] | |
| 30 | + | spec.homepage = "https://g1t.sh/acme/http-client" | |
| 31 | + | spec.metadata["source_code_uri"] = "https://g1t.sh/acme/http-client" | |
| 32 | + | spec.metadata["allowed_push_host"] = "https://g1t.sh/-/rubygems/acme" | |
| 33 | + | end | |
| 34 | + | ``` | |
| 35 | + | ||
| 36 | + | Build it and push it with an [access token](https://g1t.sh/settings/tokens) | |
| 37 | + | as the API key: | |
| 38 | + | ||
| 39 | + | ```sh | |
| 40 | + | gem build http-client.gemspec | |
| 41 | + | GEM_HOST_API_KEY=<token> gem push http-client-0.3.1.gem --host https://g1t.sh/-/rubygems/acme | |
| 42 | + | ``` | |
| 43 | + | ||
| 44 | + | To keep the key instead of passing it each time, add it to | |
| 45 | + | `~/.gem/credentials`, under the registry's address: | |
| 46 | + | ||
| 47 | + | ```yaml | |
| 48 | + | --- | |
| 49 | + | :https://g1t.sh/-/rubygems/acme: g1t_... | |
| 50 | + | ``` | |
| 51 | + | ||
| 52 | + | `allowed_push_host` keeps the gem from being pushed to rubygems.org by | |
| 53 | + | mistake. A token with full access works; one with scopes needs | |
| 54 | + | `packages:write` to push and yank, and `packages:read` to install private | |
| 55 | + | gems. | |
| 56 | + | ||
| 57 | + | The first push makes the gem. When its `source_code_uri` (or `homepage`) | |
| 58 | + | is a g1t.sh repository of the same workspace, or a repository is named | |
| 59 | + | like the gem (a gem `http_client` is also matched to a repository | |
| 60 | + | `http-client`), it is linked to that repository and has its visibility and | |
| 61 | + | roles: pushing needs Write on it. Otherwise it is the workspace's, | |
| 62 | + | private, and needs the workspace's Write base permission. See | |
| 63 | + | [who can see and publish a package](/guides/packages/#who-can-see-and-publish-a-package). | |
| 64 | + | ||
| 65 | + | The gem's page on g1t.sh shows the summary of its highest stable version. | |
| 66 | + | ||
| 67 | + | ## Install with Bundler | |
| 68 | + | ||
| 69 | + | Name the workspace's source for the gems that come from it: | |
| 70 | + | ||
| 71 | + | ```ruby | |
| 72 | + | source "https://rubygems.org" | |
| 73 | + | ||
| 74 | + | source "https://g1t.sh/-/rubygems/acme/" do | |
| 75 | + | gem "http-client", "~> 0.3" | |
| 76 | + | end | |
| 77 | + | ``` | |
| 78 | + | ||
| 79 | + | or let Bundler write that for you: | |
| 80 | + | ||
| 81 | + | ```sh | |
| 82 | + | bundle add http-client --source https://g1t.sh/-/rubygems/acme/ | |
| 83 | + | ``` | |
| 84 | + | ||
| 85 | + | For private gems, give Bundler a username (any works) and a token, for | |
| 86 | + | the source's address: | |
| 87 | + | ||
| 88 | + | ```sh | |
| 89 | + | bundle config set --global https://g1t.sh/-/rubygems/acme/ ada:<token> | |
| 90 | + | ``` | |
| 91 | + | ||
| 92 | + | Or set them once for every workspace on g1t.sh, by host: | |
| 93 | + | `bundle config set --global g1t.sh ada:<token>`, which can also come from | |
| 94 | + | the environment as `BUNDLE_G1T__SH`, as [workflows](#in-workflows) give it. | |
| 95 | + | ||
| 96 | + | `bundle install` then reads the compact index and downloads each `.gem`, | |
| 97 | + | which it checks against the SHA-256 the index names. | |
| 98 | + | ||
| 99 | + | ## Names and versions | |
| 100 | + | ||
| 101 | + | A gem's name is letters, digits, `.`, `-` and `_`, with at least one | |
| 102 | + | letter. In a workspace, names that differ only in case are one name: once | |
| 103 | + | `http-client` is pushed, `HTTP-Client` is refused. | |
| 104 | + | ||
| 105 | + | A version is pushed once, for each platform: pushing `0.3.1` again is | |
| 106 | + | refused with `409`, even after it is yanked, so bump `version` first. A | |
| 107 | + | gem built for a platform (`0.3.1-x86_64-linux`) is its own version beside | |
| 108 | + | the `ruby` one. A version with a letter in it (`0.4.0.rc1`) is a | |
| 109 | + | pre-release, which Bundler picks only when asked for. | |
| 110 | + | ||
| 111 | + | ## Yank | |
| 112 | + | ||
| 113 | + | ```sh | |
| 114 | + | GEM_HOST_API_KEY=<token> gem yank http-client --version 0.3.1 --host https://g1t.sh/-/rubygems/acme | |
| 115 | + | ``` | |
| 116 | + | ||
| 117 | + | A yanked version leaves the index, so Bundler no longer resolves to it, | |
| 118 | + | but its `.gem` is still downloaded for a `Gemfile.lock` that names it. | |
| 119 | + | Yanking needs what pushing does. The gem's page marks yanked versions, and | |
| 120 | + | someone with Admin on the linked repository (an owner, for the | |
| 121 | + | workspace's own gems) can delete a version there for good. | |
| 122 | + | ||
| 123 | + | ## Private and public gems | |
| 124 | + | ||
| 125 | + | A gem linked to a repository has the repository's visibility; one of the | |
| 126 | + | workspace's own is private until an owner makes it public on its page. | |
| 127 | + | ||
| 128 | + | | The workspace's gems | Without credentials | With credentials | | |
| 129 | + | | --- | --- | --- | | |
| 130 | + | | All public | Bundler reads the index and downloads them. | The same; the key is sent to push and yank. | | |
| 131 | + | | Some private | The registry answers `401`, and Bundler asks for credentials for the source. | Each gem the credentials' owner may see. | | |
| 132 | + | ||
| 133 | + | A private gem you cannot see looks exactly like one that does not exist. | |
| 134 | + | ||
| 135 | + | ## In workflows | |
| 136 | + | ||
| 137 | + | A workflow's `G1T_TOKEN` is the workspace's own token for the run, and can | |
| 138 | + | install and push the workspace's gems: | |
| 139 | + | ||
| 140 | + | ```yaml | |
| 141 | + | jobs: | |
| 142 | + | publish: | |
| 143 | + | runs-on: ubuntu-latest | |
| 144 | + | env: | |
| 145 | + | GEM_HOST_API_KEY: ${{ secrets.G1T_TOKEN }} | |
| 146 | + | BUNDLE_G1T__SH: g1t:${{ secrets.G1T_TOKEN }} | |
| 147 | + | steps: | |
| 148 | + | - uses: actions/checkout@v4 | |
| 149 | + | - run: bundle install && bundle exec rake test | |
| 150 | + | - run: gem build http-client.gemspec | |
| 151 | + | - run: gem push http-client-*.gem --host https://g1t.sh/-/rubygems/acme | |
| 152 | + | ``` | |
| 153 | + | ||
| 154 | + | ## Size | |
| 155 | + | ||
| 156 | + | A push is one request with the `.gem` as its body, and may hold at most | |
| 157 | + | 100 MB. Without the [g1t plan](/guides/usage-and-billing/#the-g1t-plan), a | |
| 158 | + | workspace's private packages may hold 500 MB and its public ones 10 GB, as | |
| 159 | + | for [container images](/guides/containers/#storage-and-pull-limits). A | |
| 160 | + | `.gem` is stored once, by its content. | |
| 161 | + | ||
| 162 | + | ## Errors | |
| 163 | + | ||
| 164 | + | | Error | Means | | |
| 165 | + | | --- | --- | | |
| 166 | + | | `401` | No credentials or key, or a wrong or expired token. For Bundler, set the source's credentials with `bundle config set`; for `gem push`, give `GEM_HOST_API_KEY`. | | |
| 167 | + | | `403` | Signed in, but your role or your token's scopes do not allow it, or the workspace is out of free package storage. The response says which. | | |
| 168 | + | | `404` | No such gem or version, or a private one you cannot see. | | |
| 169 | + | | `409` | That version is already pushed, or its name is taken by a gem named in another case. | | |
| 170 | + | | `422` | The push was refused: not a `.gem`, or a name or version RubyGems would not take. The response says which. | | |
| 171 | + | | `413` | The `.gem` is over 100 MB. | |