Commit

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.

syntaqxcommitted Parenta6d71e7Browse files
5 files+641−60/5 viewed
+3−0
7878 { label: 'npm', slug: 'guides/npm' },
7979 { label: 'Cargo', slug: 'guides/cargo' },
8080 { label: 'Composer', slug: 'guides/composer' },
81+ { label: 'Maven', slug: 'guides/maven' },
82+ { label: 'NuGet', slug: 'guides/nuget' },
83+ { label: 'RubyGems', slug: 'guides/rubygems' },
8184 { label: 'Go modules', slug: 'guides/go' },
8285 { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' },
8386 { label: 'Security', slug: 'guides/security' },
+271−0
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. |
+183−0
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. |
+13−6
55
66 A workspace can publish packages to g1t and install them from it, beside
77 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
1112 read from the workspace's repositories: there is nothing to upload.
1213
1314 | Registry | Address | Guide |
1516 | Container images | `g1t.sh/<workspace>/<name>` | [Container images](/guides/containers/) |
1617 | npm | `https://g1t.sh/-/npm/`, for the scope `@<workspace>` | [npm](/guides/npm/) |
1718 | 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/) |
1822 | Composer | `https://g1t.sh/-/composer/<workspace>/`, from the workspace's repositories | [Composer](/guides/composer/) |
1923 | Go | `g1t.sh/<workspace>/<repo>`, straight from git | [Go modules](/guides/go/) |
2024
3438 an npm package whose `package.json` `repository` is a g1t.sh repository
3539 of the workspace, or which is named like one (`@acme/web`), and of a
3640 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/):
3844
3945 | | Needs |
4046 | --- | --- |
94100 ## Events and the audit log
95101
96102 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
99106 `package.published`, `package.version_deleted`, `package.deleted` and
100107 `package.visibility_changed`, which [webhooks](/guides/webhooks/) can be
101108 sent: a linked package's go to its repository's webhooks and its
+171−0
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. |