diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b95333..c09d09d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,9 @@ name: Release # Uploads to Maven Central via the Central Portal. # # Unlike the other five registries in this fleet, Central has NO Trusted -# Publishing — there is no OIDC path, so this is the one workflow that needs -# real stored secrets. Doppler (project seatlayer-release, config prd) is the -# source of truth; these are operational copies pushed into Actions secrets. +# Publishing: there is no OIDC path, so this is the one workflow that needs +# real stored secrets. They are kept in the organization's release secrets +# store and copied into this repository's Actions secrets. # # This workflow PUBLISHES. pom.xml sets true, so a # tag push goes all the way to Central with no human step. @@ -78,6 +78,6 @@ jobs: run: | v="${GITHUB_REF_NAME#v}" echo "PUBLISHED to Maven Central: io.seatlayer:seatlayer-java:$v" - echo "This is permanent — Central versions cannot be replaced or deleted." + echo "This is permanent: Central versions cannot be replaced or deleted." echo "https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/$v/" echo "Search indexing lags publication by roughly 15-30 minutes." diff --git a/README.md b/README.md index f2885bb..4e84d3a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/io.seatlayer/seatlayer-java.svg)](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java) [![License: MIT](https://img.shields.io/badge/license-MIT-111827.svg)](LICENSE) -SeatLayer is interactive seating chart software built for stadium scale. Platforms embed the white-label seat picker with their own checkout; organizers sell seated events on their own website with their own payment gateway. +The official Java client for the SeatLayer API. `io.seatlayer:seatlayer-java` lets a Java or Kotlin backend inspect seat holds, price orders from server data, book reserved seats and verify webhooks, with zero runtime dependencies. SeatLayer is seating chart and reserved-seat ticketing software built for venues up to stadium scale. SeatLayer's official Java server SDK is the **trusted side** of its reserved seating and seat booking API: inspect the holds a buyer created, price from server data, and book with a stable @@ -44,7 +44,7 @@ implementation 'io.seatlayer:seatlayer-java:0.8.0' ``` Published on Maven Central as `io.seatlayer:seatlayer-java`; `0.8.0` is the current release, so no -extra repository declaration is needed. Requires Java 17 or newer. **Zero runtime dependencies** — the SDK uses +extra repository declaration is needed. Requires Java 17 or newer. **Zero runtime dependencies**: the SDK uses `java.net.http.HttpClient` and `javax.crypto.Mac` from the JDK plus a small hand-written JSON codec, so it never forces a Jackson or OkHttp version on an application that already has one. @@ -99,7 +99,7 @@ Version `0.7.0` exposes all 48 trusted organizer operations through After the test hold/book/cancel journey and matching webhook deliveries, `validateSeasonBuyerRehearsal(seasonKey)` sends no evidence body; SeatLayer discovers the retained chain automatically. Retrieved Season holds contain -inventory identity, not an authoritative amount—your platform owns package +inventory identity, not an authoritative amount. Your platform owns package price, payment, order, tax, refunds, benefits, and ticket or pass delivery. ```java @@ -132,7 +132,7 @@ if ("production".equals(System.getenv("ENV")) && !"live".equals(seatlayer.mode() ## Book reserved seats from Java **Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and -books. Never price from what the browser sent you — `retrieveHold` is authoritative. +books. Never price from what the browser sent you: `retrieveHold` is authoritative. ```java import java.math.BigDecimal; @@ -161,7 +161,7 @@ seatlayer.inventory().book(eventKey, holdId, charge.id()); **Your backend picks the seats.** Phone orders, box office, comps. ```java -// Payment already taken — book outright, so nothing is stranded if a second call fails. +// Payment already taken: book outright, so nothing is stranded if a second call fails. seatlayer.inventory().bookBestAvailable(eventKey, 2, "phone-1183"); // Or name the seats yourself. @@ -200,7 +200,7 @@ explicit privileged `ignoreChannelRestrictions` flag, and an audit `reason`. ## Listing and pagination `list()` returns one `Page` plus a cursor. When you want everything, `listAll()` pages for you and -yields as you consume it — a lazy `Iterable` rather than a `List`, because the point of paginating +yields as you consume it. It is a lazy `Iterable` rather than a `List`, because the point of paginating is to *not* hold an unbounded result set in memory. ```java @@ -217,8 +217,8 @@ for (Map event : seatlayer.events().listAll()) { ``` Listing events includes live availability `counts` by default, which costs the server one -round-trip **per event**. `listAll()` turns them off automatically — walking a whole catalogue is -exactly when you don't want that — and you can control it explicitly: +round-trip **per event**. `listAll()` turns them off automatically, since walking a whole catalogue is +exactly when you don't want that, and you can control it explicitly: ```java seatlayer.events().list(EventListOptions.builder().limit(50).counts(false).build()); @@ -226,14 +226,14 @@ seatlayer.events().list(EventListOptions.builder().limit(50).counts(false).build ## Keeping a hold alive -When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than +When an order takes longer than the checkout window (an invoice, a phone sale), extend rather than release and re-hold. Releasing first hands the seats to whoever is racing for them in between. ```java try { seatlayer.inventory().extendHold(eventKey, holdId, 10 * 60_000L); } catch (SeatLayerConflictException e) { - // Gone, expired, or at its renewal cap — the buyer has to re-pick. + // Gone, expired, or at its renewal cap: the buyer has to re-pick. } ``` @@ -278,7 +278,7 @@ public ResponseEntity handle( } // The signed body carries `at`, but nothing enforces a freshness window, so a - // captured delivery stays valid indefinitely. Deduplicate on occurrenceId — + // captured delivery stays valid indefinitely. Deduplicate on occurrenceId: // this is your replay protection, not an optimisation. if (alreadyProcessed((String) event.get("occurrenceId"))) { return ResponseEntity.ok().build(); @@ -309,7 +309,7 @@ try { } ``` -Every exception carries `status()`, `code()`, `body()`, and `requestId()` — quote the request id in +Every exception carries `status()`, `code()`, `body()`, and `requestId()`. Quote the request id in support requests. All are unchecked, so they do not force `throws` clauses through your call stack. ## Reliability @@ -373,8 +373,8 @@ Full reference: [SeatLayer Java server SDK guide](https://docs.seatlayer.io/serv Add the [`io.seatlayer:seatlayer-java` artifact](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java), construct a `SeatLayer` instance with your secret key, and call `inventory().book(...)` with the -hold id and a stable `bookingRef`. When your own backend picks the seats — phone orders, box -office, comps — `inventory().bookBestAvailable(...)` and `inventory().boxOfficeBook(...)` book +hold id and a stable `bookingRef`. When your own backend picks the seats (phone orders, box +office, comps), `inventory().bookBestAvailable(...)` and `inventory().boxOfficeBook(...)` book outright with no prior hold. A booking reference is required on every booking call, so each sale is tied to an immutable order id you can reconcile against later. @@ -393,7 +393,7 @@ retrieve it with `inventory().retrieveHold(...)`, whose item-level price, quanti are authoritative, and confirm it with `inventory().book(...)`. Use `inventory().extendHold(...)` for a long checkout instead of releasing and re-holding, which would hand the seats to whoever is racing for them. Booking is a single automatic attempt: after an -unknown network outcome you may reconcile and repeat the exact same event, hold, and `bookingRef` — +unknown network outcome you may reconcile and repeat the exact same event, hold, and `bookingRef`; seats already booked under that reference are not sold again. ### Can I use my own payment provider? diff --git a/RELEASE.md b/RELEASE.md index 112cde5..d3705f2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,302 +1,68 @@ # Releasing to Maven Central -Maven Central is the strictest registry SeatLayer publishes to, and the only one with a -**manual human verification step that has to clear before the first publish can happen at -all**. npm, PyPI, RubyGems and NuGet let you create an account and push within minutes. -Central does not: you must prove you control the `io.seatlayer` namespace, and Sonatype -reviews that proof. Budget for the wait — see step 2. +`io.seatlayer:seatlayer-java` is published to Maven Central through the Central +Portal by `.github/workflows/release.yml`. A tag push publishes all the way to +Central: `pom.xml` sets `true`, so there is no manual +approval step in the Portal. -Everything below that is a repository concern is already done. What remains is -account-, key- and DNS-shaped, and only the owner can do it. +**A version on Maven Central can never be replaced, edited or deleted.** A bad +release is fixed only by publishing a newer version. The workflow gate (tests, a +tag-versus-`` check and GPG signing) runs before anything is uploaded, +and any failure means nothing is sent. Do not weaken it. ---- +## One-time setup -## What is already satisfied in this repo +These are already in place and only need revisiting when a credential changes. -Do not redo these; they are wired into `pom.xml` and verified by `mvn verify`. +- **Namespace.** `io.seatlayer` is verified in the Central Portal by a DNS TXT + record on `seatlayer.io`. It covers every artifact under `io.seatlayer`. +- **Signing key.** Central checks each `.asc` signature against the public key on + the public keyserver network, so the public key must be published (for example + to `keyserver.ubuntu.com` and `keys.openpgp.org`) and retrievable with + `gpg --recv-keys` before a release. +- **Actions secrets** used by the workflow: -| Central requirement | Where it is handled | -| --- | --- | -| `groupId` / `artifactId` / `version`, non-SNAPSHOT | `pom.xml` — `io.seatlayer:seatlayer-java:0.1.0` | -| `name`, `description`, `url` | `pom.xml` | -| `licenses` (name + url) | `pom.xml` — MIT | -| `developers` | `pom.xml` | -| `scm` — `connection`, `developerConnection`, `url` | `pom.xml` | -| Main jar | `maven-jar-plugin`, default lifecycle | -| `-sources.jar` | `maven-source-plugin`, `attach-sources` | -| `-javadoc.jar` | `maven-javadoc-plugin`, `attach-javadocs` | -| Javadoc actually builds under strict doclint | `all,-missing`, `failOnError=true` | -| `.asc` signature on every artifact | `maven-gpg-plugin` in the `release` profile | -| Upload transport | `central-publishing-maven-plugin`, `extensions=true` | + | Secret | Purpose | + | --- | --- | + | `MAVEN_CENTRAL_USERNAME` | Central Portal user token name | + | `MAVEN_CENTRAL_PASSWORD` | Central Portal user token password | + | `MAVEN_GPG_PRIVATE_KEY` | Armored private signing key | + | `MAVEN_GPG_PASSPHRASE` | Passphrase for the signing key | ---- + Central has no Trusted Publishing (OIDC) option, so these are the only stored + release secrets across the SeatLayer server SDKs. -## 0. Pre-flight, in the repo +## Release steps -```bash -mvn -B clean verify -``` - -Must end `BUILD SUCCESS` with 48 tests passing and three jars in `target/`. - -Then flip the changelog heading in `CHANGELOG.md` from `## 0.1.0 — unreleased` to -`## 0.1.0 — `, and commit. The version on Central is permanent, so the -changelog entry describing it should not say "unreleased". - ---- - -## 1. Central Portal account - -1. Sign in at (GitHub or Google SSO is fine). -2. Account → **Generate User Token**. This returns a `` / `` pair. - These are *not* your login credentials — they are a scoped publishing token, and they - are the only thing that belongs in `settings.xml`. - -Keep the token. Regenerating it invalidates the previous one. - ---- - -## 2. Namespace verification — THE HUMAN STEP, AND THE SLOW ONE - -Central will not accept an artifact in a namespace you have not proven you control. -Nothing currently exists under `https://repo1.maven.org/maven2/io/seatlayer/` (verified -404), so this is a first-time claim and there is no shortcut. +1. Bump `` in `pom.xml` (never a `-SNAPSHOT`). +2. Add a dated entry at the top of `CHANGELOG.md`. +3. Run the gate locally: -**We publish under `io.seatlayer`, which is verified by DNS.** + ```bash + mvn -B clean verify + ``` -In the Portal: **Namespaces → Add Namespace → `io.seatlayer`**. The Portal shows a -verification key, something like `abc123xyz`. +4. Merge the release commit to `main`, then tag it and push the tag: -Add it as a TXT record on the apex of the domain: + ```bash + git tag v0.8.1 && git push origin v0.8.1 + ``` -``` -seatlayer.io. TXT "abc123xyz" -``` +The workflow runs `mvn -B verify`, refuses to publish if the tag and the pom +version disagree, then runs `mvn -B -Prelease deploy`, which signs the jar, +sources jar, javadoc jar and pom and uploads them to the Central Portal. -The domain is on Cloudflare, so this is: DNS → Records → Add record → type TXT, name `@`, -content the verification key. Then press **Verify Namespace** in the Portal. +## Verify -Confirm the record is actually visible before pressing verify, or the check fails and you -wait again: +The artifact appears on `repo1.maven.org` within minutes; search indexing can lag +by up to a few hours. ```bash -dig +short TXT seatlayer.io +curl -sI https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/0.8.1/seatlayer-java-0.8.1.pom ``` -**Wall-clock cost.** Cloudflare publishes TXT records in well under a minute, so the DNS -side is effectively instant. The Portal's own check is usually minutes; first-time -namespace claims are sometimes queued for Sonatype staff review, which is where the -multi-hour-to-a-business-day tail comes from. Start this step first — it is the only one -that can block for a day, and every other step below takes minutes. - -Once verified, the namespace covers `io.seatlayer` and everything under it, so the other -JVM artifacts SeatLayer may publish later need no further verification. - -### Why `io.seatlayer` and not `io.github.seatlayer` - -`io.github.` is verified by creating a temporary public GitHub repo whose name is -the verification code — no DNS, no review queue, typically verified in under a minute. -It is genuinely cheaper *in setup*. - -It is still the wrong choice here: - -- SeatLayer owns `seatlayer.io`, so the DNS proof is available at zero cost beyond one - TXT record the owner can add in the Cloudflare dashboard in about a minute. -- `io.seatlayer` is the coordinate every other SeatLayer surface already advertises. The - README, the docs site and the published `pom.xml` all say `io.seatlayer`. Changing it - to `io.github.seatlayer` means the install snippet no longer matches the brand. -- **The groupId is permanent in practice.** A published artifact's coordinates can never - be changed, only abandoned and re-published under a new groupId, orphaning everyone on - the old one. Picking the cheap namespace now to save ten minutes buys a migration later. - -The cost difference is one DNS record versus one throwaway repo. Pay the DNS record. - ---- - -## 3. GPG key — generate it AND publish it to a keyserver - -**This is the step people miss.** Signing locally is not enough. Central fetches your -*public* key from the public keyserver network to check the signature. A perfectly valid -signature from a key nobody can find is rejected with a validation error that reads like a -signing failure, and the usual reaction is to re-sign rather than to distribute the key. - -Generate the key: - -```bash -gpg --full-generate-key -# RSA and RSA, 4096 bits, no expiry (or a long one you will actually track), -# real name + an @seatlayer.io address -``` - -Find its id: +Then resolve it from a clean local repository in a scratch project: ```bash -gpg --list-secret-keys --keyid-format=long -# sec rsa4096/A1B2C3D4E5F6A7B8 2026-08-04 [SC] -# ^^^^^^^^^^^^^^^^ the key id +mvn -Dmaven.repo.local=/tmp/m2-clean dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.8.1 ``` - -**Publish the public half** — do all of these, they do not fully sync with each other: - -```bash -gpg --keyserver keyserver.ubuntu.com --send-keys A1B2C3D4E5F6A7B8 -gpg --keyserver keys.openpgp.org --send-keys A1B2C3D4E5F6A7B8 -gpg --keyserver pgp.mit.edu --send-keys A1B2C3D4E5F6A7B8 -``` - -Then prove it is actually retrievable from somewhere other than your own machine, before -you rely on it: - -```bash -gpg --keyserver keyserver.ubuntu.com --recv-keys A1B2C3D4E5F6A7B8 -``` - -Propagation is usually seconds to a few minutes. If you publish and immediately deploy, -you can lose to that race — wait for a successful `--recv-keys` first. - -Back up the secret key somewhere durable. Losing it does not invalidate what is already -published, but it means future releases are signed by a different key. - ---- - -## 4. Credentials — `~/.m2/settings.xml`, never this repo - -No secret goes in the repository. The `release` profile reads the passphrase from a -property or the environment; the token lives in your user-level settings. - -`~/.m2/settings.xml`: - -```xml - - - - - central - TOKEN_USERNAME_FROM_STEP_1 - TOKEN_PASSWORD_FROM_STEP_1 - - - - - - gpg - - A1B2C3D4E5F6A7B8 - YOUR_KEY_PASSPHRASE - - - - - gpg - - -``` - -`chmod 600 ~/.m2/settings.xml`. - -Prefer not to keep the passphrase on disk at all? Drop the `gpg` profile above and pass it -per-invocation instead — the plugin also honours `MAVEN_GPG_PASSPHRASE`: - -```bash -export MAVEN_GPG_KEY=A1B2C3D4E5F6A7B8 -read -rs MAVEN_GPG_PASSPHRASE && export MAVEN_GPG_PASSPHRASE -``` - -Maven can also encrypt the token password (`mvn --encrypt-password`) if you would rather -not have it in cleartext. - ---- - -## 5. Publish - -Confirm signing works and the `.asc` files are produced **before** uploading anything: - -```bash -mvn -Prelease clean verify -ls target/*.asc -``` - -You should see four signatures — jar, sources, javadoc, and the pom. If `target/*.asc` is -empty, the `release` profile did not activate or GPG did not run; fix that now, because -Central rejects an unsigned bundle. - -Then, the release: - -```bash -mvn -Prelease clean deploy -``` - -That is the whole publish command. It builds, tests, signs, bundles and uploads, and -because `waitUntil=validated` it blocks until Central has finished validating and reports -any rejection to your terminal rather than silently exiting 0. - -**It does not go live yet.** `autoPublish` is `false` on purpose. The deployment lands in -the Portal as **VALIDATED** and waits. Go to → -**Deployments**, check the artifact list and the coordinates, and press **Publish**. - -Up to that button, a deployment can be dropped and redone freely. After it, see step 7. - -Sync to `repo1.maven.org` takes a few minutes; appearing in the search UI can take a few -hours longer. Both are normal — the artifact is resolvable well before search finds it. - ---- - -## 6. Verify it resolves from a clean local repository - -Do not verify against your own `~/.m2`; it already has the artifact from the local build -and will succeed regardless of whether the publish worked. Point Maven at an empty repo: - -```bash -mvn dependency:get \ - -Dartifact=io.seatlayer:seatlayer-java:0.1.0 \ - -Dmaven.repo.local=/tmp/central-check \ - -DremoteRepositories=central::::https://repo1.maven.org/maven2 -``` - -Then confirm the classified artifacts and signatures are all there too: - -```bash -mvn dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.1.0:jar:sources \ - -Dmaven.repo.local=/tmp/central-check -mvn dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.1.0:jar:javadoc \ - -Dmaven.repo.local=/tmp/central-check -curl -sI https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/0.1.0/seatlayer-java-0.1.0.jar.asc \ - | head -1 -``` - -Finally, the thing that actually matters — a consumer can compile against it. In an empty -directory with the README's install snippet as the only dependency, `mvn compile` on a -one-line file that does `new SeatLayer("sk_test_x")` should resolve and build. - ---- - -## 7. What is IRREVERSIBLE - -Read this before pressing **Publish**. - -- **A released version can never be replaced, edited or deleted.** Not by you, not by - Sonatype support. `io.seatlayer:seatlayer-java:0.1.0` is that content, permanently. This - is deliberate: the entire JVM ecosystem assumes Central coordinates are immutable, and - build reproducibility depends on it. -- **A bad release is fixed only by superseding it** — publish `0.1.1`. The broken `0.1.0` - stays downloadable forever. You can mark it deprecated in documentation; you cannot - remove it. -- **The groupId is effectively permanent too.** Once consumers depend on - `io.seatlayer:seatlayer-java`, changing the coordinate strands every one of them on an - artifact that stops receiving updates. -- **Before the Publish button, nothing is committed.** A VALIDATED deployment can be - dropped in the Portal with no trace. That gap is the entire safety margin, which is why - `autoPublish` is `false` in `pom.xml` — do not set it to `true` for convenience. - -So: check the coordinates, the version, and the contents of the three jars in the Portal's -deployment view. Then publish. - ---- - -## Subsequent releases - -1. Bump `` in `pom.xml`, update `CHANGELOG.md`, commit, tag. -2. `mvn -Prelease clean deploy` -3. Approve in the Portal. - -Namespace verification and key distribution are one-time. Only step 5 onward repeats. diff --git a/pom.xml b/pom.xml index 91139bc..da7ecae 100644 --- a/pom.xml +++ b/pom.xml @@ -137,10 +137,10 @@ central