From 91081511d484a46477afccabfe711016a3424b25 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:17:03 -0600 Subject: [PATCH 1/5] Stops printing the Maven settings file in the release workflow The publish job writes a settings.xml naming the Central Portal server and then printed it. The file holds only two ${env.NAME} references, never a value, but the publish job is the one that carries the credentials, and no step there needs to print a file that names them. The step now writes the file and prints nothing. Refs: uxjv-578 --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78f7e1a..1f0f62c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -458,7 +458,6 @@ jobs: SETTINGS - cat "${RUNNER_TEMP}/settings.xml" # The only step that reads the secrets (the only `secrets` expressions # in this file), into its own environment. The `release` profile rebuilds From effe0dc938b41b5f6706729b3ff93b68a456a860 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:17:12 -0600 Subject: [PATCH 2/5] Says to wait for repo1 before re-running the publish job alone The publish job decides between publishing and checking the listed jars by reading the version's POM at repo1.maven.org. repo1 can lag the Central Portal by some minutes after the Portal reports a deployment published, so a re-run of publish inside that window finds the version not listed and builds, signs and uploads it again. The header's re-run paragraph now says to wait until repo1 answers 200 for the POM before that re-run. Refs: uxjv-578 --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f0f62c..72739f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,6 +88,10 @@ name: Release # run whose `publish` job failed after the upload, the maintainer first # looks at the deployment in the Portal: a deployment still validating or # publishing is waited for, and one that failed validation is dropped there. +# A deployment the Portal reports published is then waited for at +# repo1.maven.org, until the version's POM answers 200 there: repo1 can lag +# the Portal by some minutes, and a re-run inside that window finds the +# version not listed and builds, signs and uploads it again. # "Re-run failed jobs" then re-runs `publish` alone, with the artifact and # the SHA-256 `verify` recorded (while the artifact is kept). If Maven # Central lists the version by then, `publish` reports it, downloads the From 953cfa82ee3da60c98764b1121d77d2ac0e2acf1 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:17:19 -0600 Subject: [PATCH 3/5] Names a Portal validation failure among the commit-caused failures The release workflow's header lists the failures the tagged commit itself causes, which no re-run can turn green. A deployment the Central Portal fails on validation because of the commit, such as missing POM metadata or a file the build does not produce, is one of them: dropped in the Portal and re-run, it uploads the same files and fails the same way. The header now names it. Refs: uxjv-578 --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72739f7..0b13685 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,8 +102,10 @@ name: Release # signs and uploads again. Re-running every job instead re-runs `verify`, # which refuses a version Maven Central lists, so that re-run stays red. A # failure the tagged commit itself causes (it is not on the default branch, -# the tag and pom.xml name different versions, the gate is red) fails the -# same way on every re-run. +# the tag and pom.xml name different versions, the gate is red, or the +# Portal fails the deployment's validation for something in the commit, +# such as the POM's metadata or a file the build does not produce) fails +# the same way on every re-run. # # Remedies. A version Maven Central lists is permanent: Maven Central never # deletes or replaces a published file, so a broken release is followed by From c49a44040aa59ff4317656de07651772e85f829b Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:19:15 -0600 Subject: [PATCH 4/5] Prepares the 1.0.0 release The project's version in pom.xml moves from 0.1.0-SNAPSHOT to 1.0.0, the version the README's Maven and Gradle lines already name, so the tag v1.0.0 at this commit is the one the release workflow accepts. project.build.outputTimestamp stays 2026-10-21T00:00:00Z, the release date; two clean builds of the main, sources and javadoc jars give byte-identical files, which the publish job relies on when it compares its rebuild with the jars the verify job built. CHANGELOG.md gains the 1.0.0 section in place of the Unreleased note: the coordinate, the module, the Java 17 floor, the scope with every public type and its main members as the built jar declares them, and the corpus tag v2.10.0. The four fragments are promoted into its Added list in file-name order, their bullets unchanged, and deleted. Refs: uxjv-578 --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++------ changelog.d/uxjv-5tk.md | 6 ------ changelog.d/uxjv-94x.md | 6 ------ changelog.d/uxjv-iq8.md | 4 ---- changelog.d/uxjv-nyt.md | 6 ------ pom.xml | 2 +- 6 files changed, 42 insertions(+), 29 deletions(-) delete mode 100644 changelog.d/uxjv-5tk.md delete mode 100644 changelog.d/uxjv-94x.md delete mode 100644 changelog.d/uxjv-iq8.md delete mode 100644 changelog.d/uxjv-nyt.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5e833..3d27eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,44 @@ # Changelog -## Unreleased +## 1.0.0 - 2026-10-21 -The Java port of uxid: prefixed, K-sortable identifiers a person can read, -wire-compatible with the Elixir `uxid` package. No version has been released -yet. The build is in place: the Maven coordinate `systems.riddler:uxid`, the -module and package `systems.riddler.uxid`, a Java 17 floor and the build gate. -The generation, decoding and validity checks arrive in later changes. +The first release of the Java port of uxid: prefixed, K-sortable identifiers +a person can read, wire-compatible with the Elixir `uxid` package. The Maven +coordinate is `systems.riddler:uxid`, and the module and its one exported +package are `systems.riddler.uxid`. It runs on Java 17 or later and has no +runtime dependencies. It covers time-based generation and deterministic ids +(`Uxid.generate()` and `Uxid.generate(UxidOptions)`; `UxidOptions.builder()` +and the `UxidOptions.Builder` methods `prefix`, `size`, `randSize`, +`compactTime`, `letterCase`, `delimiter`, `time`, `rand`, `byteSource` and +`from`; the enums `Size` and `LetterCase`; the `ByteSource` interface), +decoding (`Uxid.decode(String)` and `Uxid.decode(String, DecodeOptions)`; +`DecodeOptions.builder()` and the `DecodeOptions.Builder` methods `delimiter` +and `clock`; the sealed `DecodeResult` with its records +`DecodeResult.Decoded`, read through `prefix()`, `size()`, `compact()`, +`deterministic()` and `time()`, and `DecodeResult.Invalid`, read through +`reason()`; the `InvalidReason` enum), the validity checks (`Uxid.isValid` +and `Uxid.isDeterministic`), the monotonic generator (`Monotonic`, built with +`new Monotonic()` or `new Monotonic(DrawSource)`, with its `generate()` and +`generate(UxidOptions)` and the `DrawSource` interface's `seed` and `step`), +and the generation refusals (`UxidException` and its `code()`, an +`ErrorCode`). It is checked against the conformance corpus vendored from +riddler/uxid-ex at the tag `v2.10.0` (`conformance/SOURCE.json` records the +tag, its commit and the corpus hash), and every case in it passes with no +skip list. + +### Added + +- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase, also within one millisecond and when the clock moves back (the pair of time and random value increases, and the id string with it within one letter case, delimiter and time form; a fresh seed after a later clock or an overflow can be a smaller random value on its own): `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form. +- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase in the order the calls take the lock. Its state grows by one entry for each distinct prefix and random width it is given and lives as long as the generator. Nothing is kept in a static field, and two generators never affect each other. +- `DrawSource` replaces a generator's seeds and steps to replay known draws; by default they come from each call's byte source, the step masked from its bytes with no modulo bias. +- `Monotonic.generate(UxidOptions)` refuses a `from` input with `from_with_monotonic` and fixed random bytes with `rand_with_monotonic`. A call that throws leaves the generator's state as it was. +- `Uxid.generate()` makes a time-based id with no prefix, size `xl`, a standard time read from the clock and ten random bytes from `SecureRandom`. +- `Uxid.generate(UxidOptions)` takes a prefix (`""` is none), a `Size` (`Size.parse` reads `xs`, `s`, `m`, `l`, `xl` and the long spellings `xsmall` to `xlarge`), a random width, compact time, a `LetterCase`, a delimiter (`_` by default), a fixed time and fixed random bytes, the last for reproducible test vectors only. +- `ByteSource`, a functional interface taking a count, supplies the random bytes; the default, `ByteSource.secureRandom()`, draws from `SecureRandom`. +- A generation request the wire format refuses throws `UxidException`, an `IllegalArgumentException` whose `code()` is an `ErrorCode`; each code's `wireName()` is its name in the wire format's closed set (`compact_time_out_of_range`, `rand_width_mismatch`, `rand_size_too_large`, `from_with_monotonic`, `rand_with_from`, `rand_with_monotonic`). +- `UxidOptions.Builder.from(String)` asks for a deterministic id: the same prefix, input and size always give the same id, a `z` (or `Z`) marker followed by hash characters from SHA-256 at the size's standard length, such as `usr_zcvt7epac0t1ebcsjfyf7cwz25` for `usr` and `alice@example.com` at `xl`. +- A deterministic request does not read the clock, the time form, the random width or the byte source; fixed random bytes with it are refused with `rand_with_from`, and an input or prefix holding an unpaired surrogate, which has no UTF-8 encoding, is refused with `IllegalArgumentException` rather than hashed with a replacement character. +- `Uxid.decode(String)` and `Uxid.decode(String, DecodeOptions)` read an id back to a `DecodeResult`: a `DecodeResult.Decoded` with the prefix, the size, whether the time is compact, whether the body is deterministic and the time in milliseconds, or a `DecodeResult.Invalid` whose `InvalidReason` is `empty`, `invalid_length` or `invalid_character`, the first rule the body breaks in that order. Malformed input is answered, never thrown. +- `DecodeOptions` takes the delimiter and a `java.time.Clock` that supplies a compact time's high bits; a compact time more than one day ahead of the clock is read from the 2^40 ms window before, which with a clock in the first window after the Unix epoch is a negative time. +- The body lengths 10 and 18 always decode as standard time, so a compact `xs` or compact `m` id decodes to its size, not compact, with a time that is not the one it was generated at. +- `Uxid.isValid(String)` and `Uxid.isValid(String, String delimiter, String requiredPrefix)` answer whether a string is a well-formed id and, given a prefix, carries exactly that prefix; `Uxid.isDeterministic(String)` and `Uxid.isDeterministic(String, String delimiter)` answer whether it is a well-formed deterministic id. Neither reads a clock. diff --git a/changelog.d/uxjv-5tk.md b/changelog.d/uxjv-5tk.md deleted file mode 100644 index 0a9cfb9..0000000 --- a/changelog.d/uxjv-5tk.md +++ /dev/null @@ -1,6 +0,0 @@ -### Added - -- `Monotonic` is a generator the caller holds whose ids for one prefix and random width increase, also within one millisecond and when the clock moves back (the pair of time and random value increases, and the id string with it within one letter case, delimiter and time form; a fresh seed after a later clock or an overflow can be a smaller random value on its own): `generate()` and `generate(UxidOptions)` keep the last time and random value per prefix and random width, step the random field by a value drawn uniformly from 1 to 2^(4w) within a millisecond, and move the time one millisecond forward with a fresh seed when the field overflows. A monotonic `xs` uses compact time unless the options set the time form. -- One `Monotonic` may be shared between threads: a lock inside the instance orders the calls, so its ids still increase in the order the calls take the lock. Its state grows by one entry for each distinct prefix and random width it is given and lives as long as the generator. Nothing is kept in a static field, and two generators never affect each other. -- `DrawSource` replaces a generator's seeds and steps to replay known draws; by default they come from each call's byte source, the step masked from its bytes with no modulo bias. -- `Monotonic.generate(UxidOptions)` refuses a `from` input with `from_with_monotonic` and fixed random bytes with `rand_with_monotonic`. A call that throws leaves the generator's state as it was. diff --git a/changelog.d/uxjv-94x.md b/changelog.d/uxjv-94x.md deleted file mode 100644 index 7edb872..0000000 --- a/changelog.d/uxjv-94x.md +++ /dev/null @@ -1,6 +0,0 @@ -### Added - -- `Uxid.generate()` makes a time-based id with no prefix, size `xl`, a standard time read from the clock and ten random bytes from `SecureRandom`. -- `Uxid.generate(UxidOptions)` takes a prefix (`""` is none), a `Size` (`Size.parse` reads `xs`, `s`, `m`, `l`, `xl` and the long spellings `xsmall` to `xlarge`), a random width, compact time, a `LetterCase`, a delimiter (`_` by default), a fixed time and fixed random bytes, the last for reproducible test vectors only. -- `ByteSource`, a functional interface taking a count, supplies the random bytes; the default, `ByteSource.secureRandom()`, draws from `SecureRandom`. -- A generation request the wire format refuses throws `UxidException`, an `IllegalArgumentException` whose `code()` is an `ErrorCode`; each code's `wireName()` is its name in the wire format's closed set (`compact_time_out_of_range`, `rand_width_mismatch`, `rand_size_too_large`, `from_with_monotonic`, `rand_with_from`, `rand_with_monotonic`). diff --git a/changelog.d/uxjv-iq8.md b/changelog.d/uxjv-iq8.md deleted file mode 100644 index f24b1bc..0000000 --- a/changelog.d/uxjv-iq8.md +++ /dev/null @@ -1,4 +0,0 @@ -### Added - -- `UxidOptions.Builder.from(String)` asks for a deterministic id: the same prefix, input and size always give the same id, a `z` (or `Z`) marker followed by hash characters from SHA-256 at the size's standard length, such as `usr_zcvt7epac0t1ebcsjfyf7cwz25` for `usr` and `alice@example.com` at `xl`. -- A deterministic request does not read the clock, the time form, the random width or the byte source; fixed random bytes with it are refused with `rand_with_from`, and an input or prefix holding an unpaired surrogate, which has no UTF-8 encoding, is refused with `IllegalArgumentException` rather than hashed with a replacement character. diff --git a/changelog.d/uxjv-nyt.md b/changelog.d/uxjv-nyt.md deleted file mode 100644 index b05ad69..0000000 --- a/changelog.d/uxjv-nyt.md +++ /dev/null @@ -1,6 +0,0 @@ -### Added - -- `Uxid.decode(String)` and `Uxid.decode(String, DecodeOptions)` read an id back to a `DecodeResult`: a `DecodeResult.Decoded` with the prefix, the size, whether the time is compact, whether the body is deterministic and the time in milliseconds, or a `DecodeResult.Invalid` whose `InvalidReason` is `empty`, `invalid_length` or `invalid_character`, the first rule the body breaks in that order. Malformed input is answered, never thrown. -- `DecodeOptions` takes the delimiter and a `java.time.Clock` that supplies a compact time's high bits; a compact time more than one day ahead of the clock is read from the 2^40 ms window before, which with a clock in the first window after the Unix epoch is a negative time. -- The body lengths 10 and 18 always decode as standard time, so a compact `xs` or compact `m` id decodes to its size, not compact, with a time that is not the one it was generated at. -- `Uxid.isValid(String)` and `Uxid.isValid(String, String delimiter, String requiredPrefix)` answer whether a string is a well-formed id and, given a prefix, carries exactly that prefix; `Uxid.isDeterministic(String)` and `Uxid.isDeterministic(String, String delimiter)` answer whether it is a well-formed deterministic id. Neither reads a clock. diff --git a/pom.xml b/pom.xml index 4b9c16d..afd0a6e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ systems.riddler uxid - 0.1.0-SNAPSHOT + 1.0.0 jar uxid From 7d9247fc6c48e607c80036e4029d22c80b5c5712 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Wed, 21 Oct 2026 17:19:29 -0600 Subject: [PATCH 5/5] States what version 1.0.0 is in the agent instructions The instructions described 1.0.0 as the package the repository was being built into and said the sentences about it described a target, not the tree. The tree now holds that package at version 1.0.0, so the section says so and names how it reaches Maven Central: the release workflow publishes it when the tag v1.0.0 is pushed. Refs: uxjv-578 --- CLAUDE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3369f61..da69039 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,8 +145,8 @@ Also avoid `bd edit`, which opens `$EDITOR` and blocks. Use ## What this project is -`uxid` for Java. Version 1.0.0, the package this repository is being built -into, makes prefixed, K-sortable, Stripe-style identifiers a person can read, +`uxid` for Java. Version 1.0.0, the package this repository holds, makes +prefixed, K-sortable, Stripe-style identifiers a person can read, copy and route back to their resource, wire-compatible with the Elixir reference `uxid` and checked against its conformance corpus. It covers generation (random, deterministic and monotonic), decoding and the validity @@ -166,9 +166,9 @@ release workflow (`.github/workflows/release.yml`). The package holds time-based and deterministic generation (`Uxid.generate` and `UxidOptions`), decoding (`Uxid.decode`, `DecodeOptions` and `DecodeResult`), the validity checks (`Uxid.isValid` and `Uxid.isDeterministic`) and the monotonic generator -(`Monotonic`, with its `DrawSource`). The sentences in this file about the -1.0.0 package describe the target, not the current tree: it is not yet -published. +(`Monotonic`, with its `DrawSource`). `pom.xml` states the version 1.0.0, +and the release workflow publishes it to Maven Central when the tag `v1.0.0` +is pushed. - **The package is `systems.riddler:uxid`.** Version 1.0.0 is its first release on Maven Central.