Skip to content

Commit 565bf04

Browse files
authored
chore: release v1.2.0 (#922)
Version bump (Cargo.toml, openapi.rs) and CHANGELOG cut. 20 PRs since v1.1.0: cross-repo blob mount, resumable downloads for all formats, 12 bug fixes (Docker upload sessions, Maven metadata, PyPI dates.json leak, Conan v1/ping, namespace isolation, retention scheduler, auth token atomicity, storage timeouts), 1 perf improvement, 1 security update (h2), CI per-PR test images.
1 parent 7be094e commit 565bf04

4 files changed

Lines changed: 30 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
# Changelog
22
## [Unreleased]
33

4+
## [1.2.0] - 2026-08-23
5+
46
### Added
5-
- **CI builds a per-PR test image** — every non-fork PR now pushes `ghcr.io/getnora-io/nora:pr-<number>` (alpine, amd64) after the `test` job passes, and auto-comments the `docker pull` / `docker run` command on the PR so reviewers can test without building locally. Fork PRs are skipped (no `packages:write` token) (#906).
6-
- **Per-PR test images are now garbage-collected** — a `pr-image-cleanup` workflow deletes `pr-<number>` from GHCR when its PR closes, plus a daily sweep removes any `pr-*` orphan older than 7 days (GHCR has no native tag TTL) (#909).
7-
- **Resumable downloads (`Range` / `206 Partial Content`) for every format** — what Docker blob GET gained in #657 now covers all artifact payloads: maven release artifacts, npm tarballs, pypi files, gems, cargo `.crate`s, go module zips, nuget `.nupkg`s, terraform provider/module archives, conan blobs, deb/rpm packages, ansible collections, pub archives, and raw files. A single-range request is served straight from the storage backend's native ranged read (local file seek, S3/GCS ranged GET) via a shared helper, so an interrupted `curl -C -`/pip/apt download resumes instead of restarting; a resume at end-of-file gets the RFC 9110 `416` + `Content-Range: bytes */{size}` that tells the client it already has everything (previously Docker re-served the full blob), and a failed ranged read falls back to the full 200 instead of a 500. Full-200 artifact responses advertise `Accept-Ranges: bytes`. Mutable content (maven-metadata.xml, packuments, indexes, `dists/`, `repodata/`) neither advertises nor honors ranges — a resumed range across a rewrite would splice two generations. For the same reason `raw`, the one overwritable format, honors `If-Range` against its pin ETag. A partial body cannot be re-hashed, so a ranged serve carries no server-side integrity check (the #657 precedent — the client's own lockfile/checksum covers it), and on formats where the digest-quarantine gate needs the whole object a range request under an active quarantine policy falls back to the gated full response rather than bypassing it.
7+
- **Docker cross-repo blob mount (`?mount=&from=`)**`POST /v2/{name}/blobs/uploads/?mount={digest}&from={repo}` copies a blob from another repository on the same registry instead of re-uploading it. Returns `201 Created` with `Location` on success, falls back to a regular upload session if the source blob is missing (#917).
8+
- **Resumable downloads (`Range` / `206 Partial Content`) for every format** — what Docker blob GET gained in #657 now covers all artifact payloads: maven release artifacts, npm tarballs, pypi files, gems, cargo `.crate`s, go module zips, nuget `.nupkg`s, terraform provider/module archives, conan blobs, deb/rpm packages, ansible collections, pub archives, and raw files. A single-range request is served straight from the storage backend's native ranged read (local file seek, S3/GCS ranged GET) via a shared helper, so an interrupted `curl -C -`/pip/apt download resumes instead of restarting; a resume at end-of-file gets the RFC 9110 `416` + `Content-Range: bytes */{size}` that tells the client it already has everything (previously Docker re-served the full blob), and a failed ranged read falls back to the full 200 instead of a 500. Full-200 artifact responses advertise `Accept-Ranges: bytes`. Mutable content (maven-metadata.xml, packuments, indexes, `dists/`, `repodata/`) neither advertises nor honors ranges — a resumed range across a rewrite would splice two generations. For the same reason `raw`, the one overwritable format, honors `If-Range` against its pin ETag. A partial body cannot be re-hashed, so a ranged serve carries no server-side integrity check (the #657 precedent — the client's own lockfile/checksum covers it), and on formats where the digest-quarantine gate needs the whole object a range request under an active quarantine policy falls back to the gated full response rather than bypassing it (#893).
9+
- **CI builds a per-PR test image** — every non-fork PR now pushes `ghcr.io/getnora-io/nora:pr-<number>` (alpine, amd64) after the `test` job passes, and auto-comments the `docker pull` / `docker run` command on the PR so reviewers can test without building locally. Fork PRs are skipped (no `packages:write` token) (#908).
10+
- **Per-PR test images are now garbage-collected** — a `pr-image-cleanup` workflow deletes `pr-<number>` from GHCR when its PR closes, plus a daily sweep removes any `pr-*` orphan older than 7 days (GHCR has no native tag TTL) (#910).
811

912
### Fixed
10-
- **Cancelling a blob upload frees the session instead of leaking it** — `DELETE /v2/{name}/blobs/uploads/{uuid}`, the OCI cancel verb, was never routed: the upload dispatcher matched only `PATCH` and `PUT`, so a client that correctly cancelled got `405 Method Not Allowed` and its session stayed in the map until the 30-minute TTL, still holding one of `max_upload_sessions`. Concurrent CI pushes then filled the ceiling with dead entries and rejected each other with `TOOMANYREQUESTS` while barely any upload was actually in flight — a push that normally takes ~1.5 min stretched past 19 min, nearly all of it re-transferring blobs that were refused at the end. `DELETE` now removes the session and its temp file and answers `204 No Content` (`404` if the session is unknown, `400` on a repository mismatch, matching the `PATCH`/`PUT` name check). Two supporting fixes: a rejected `POST` no longer leaves behind the zero-byte temp file it created before the limit check, and the `429`'s `Retry-After` is jittered over 3–10s instead of a fixed 5s, so refused clients don't re-synchronize onto one cadence and return as a herd. New gauges `nora_upload_sessions` and `nora_upload_in_flight` expose the session-map size and the count of uploads actually streaming, so the gap between them — the idle-session backlog this bug produced — is measurable rather than inferred from client logs.
11-
- **Maven keeps server-generated artifact metadata authoritative** — a Maven client that re-uploads a stale artifact-level `maven-metadata.xml` after a concurrent deploy no longer overwrites the version list NORA generates: an uploaded artifact-level metadata document (and its checksums) is recognized by its shape and dropped, while version-level (SNAPSHOT) and group-level (plugin) metadata are still stored verbatim. On a proxy refresh, locally hosted versions are merged into the refreshed upstream document instead of being replaced by it, and the `.md5`/`.sha1`/`.sha256`/`.sha512` sidecars are recomputed from the merged document. The proxy-side merge runs under the same `publish_lock` as the upload-side regeneration, so the document and its checksums are written as one critical section and stay mutually consistent under concurrent fetch and publish (#886).
13+
- **Maven keeps server-generated artifact metadata authoritative** — a Maven client that re-uploads a stale artifact-level `maven-metadata.xml` after a concurrent deploy no longer overwrites the version list NORA generates: an uploaded artifact-level metadata document (and its checksums) is recognized by its shape and dropped, while version-level (SNAPSHOT) and group-level (plugin) metadata are still stored verbatim. On a proxy refresh, locally hosted versions are merged into the refreshed upstream document instead of being replaced by it, and the `.md5`/`.sha1`/`.sha256`/`.sha512` sidecars are recomputed from the merged document. The proxy-side merge runs under the same `publish_lock` as the upload-side regeneration, so the document and its checksums are written as one critical section and stay mutually consistent under concurrent fetch and publish (#887).
14+
- **Docker returns OCI-conformant 429 for concurrent-upload limit**`POST /v2/{name}/blobs/uploads/` now returns an OCI `TOOMANYREQUESTS` error body with `Retry-After` header when the concurrent-upload ceiling is reached, instead of a plain status code that some clients could not parse (#895).
15+
- **Object-store timeouts sized for client-paced streaming**`put` and `get` operations on the object-store backend now use timeouts proportional to the expected transfer size, so large blob uploads on slow links no longer time out spuriously (#894).
16+
- **Cancelling a blob upload frees the session instead of leaking it**`DELETE /v2/{name}/blobs/uploads/{uuid}`, the OCI cancel verb, was never routed: the upload dispatcher matched only `PATCH` and `PUT`, so a client that correctly cancelled got `405 Method Not Allowed` and its session stayed in the map until the 30-minute TTL, still holding one of `max_upload_sessions`. `DELETE` now removes the session and its temp file and answers `204 No Content`. Two supporting fixes: a rejected `POST` no longer leaves behind the zero-byte temp file, and the `429`'s `Retry-After` is jittered over 3–10 s (#897).
17+
- **npm self-prime metadata on tarball download**`ensure_npm_metadata_cached` fetches and caches the packument on a tarball cache-miss so `trust_upstream_dates` matures old npm artifacts correctly; includes `is_internal_namespace` guard (#68) (#903).
18+
- **PyPI internal bookkeeping files excluded from simple index and downloads**`dates.json` (used by `trust_upstream_dates` quarantine) was leaking into PEP 503/691 package listings and was directly downloadable; strict clients like `uv` require hashes on every file entry, causing parse failures. Both listing and download paths now filter via `is_valid_pypi_filename()` (#891).
19+
- **PyPI `ensure_pypi_dates_cached` no longer leaks internal package names upstream** — the function was missing the `is_internal_namespace` guard that Cargo and npm already had, causing internal-namespace package names to be sent to external upstream registries (#68 dependency confusion surface) (#905).
20+
- **Conan v1/ping route added** — Conan 2.x client hard-codes `GET /v1/ping` in `ClientV2Router.ping()` before any v2 API calls; the missing route returned 404, causing the client to refuse further interaction (#901).
21+
- **Docker fails closed on transient storage errors in manifest reads** — a transient storage error during manifest GET now returns 500 instead of 404, preventing clients from interpreting a storage hiccup as a missing manifest (#911).
22+
- **Atomic token-file writes; store errors answer 503, not 401** — token CRUD operations use atomic file writes (write-to-temp + rename), and a storage I/O error during token validation now returns `503 Service Unavailable` instead of `401 Unauthorized` which would cause clients to discard valid credentials (#912).
23+
- **Retention and GC unified into one scheduler** — retention and GC no longer race for the cleanup lock; a single scheduler runs retention first, then GC, so retention actually executes instead of being starved by GC (#914).
24+
25+
### Performance
26+
- **rpm/deb index-rebuild sidecars read concurrently** — sidecar files are read in parallel during index regeneration, reducing rebuild time on repositories with many packages (#913).
27+
28+
### Security
29+
- **h2 updated 0.4.13 → 0.4.17** — addresses RUSTSEC-2026-0258 (#916).
30+
31+
### Docs
32+
- **README: expanded supported registries table and fixed legend link** (#900).
33+
- **Documented `docker_anon_pull` in env example and llms.txt** (#899).
1234

1335
## [1.1.0] - 2026-07-26
1436

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ members = [
66
]
77

88
[workspace.package]
9-
version = "1.1.0"
9+
version = "1.2.0"
1010
edition = "2021"
1111
rust-version = "1.88"
1212
license = "MIT"

nora-registry/src/openapi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::AppState;
2121
#[openapi(
2222
info(
2323
title = "Nora",
24-
version = "1.1.0",
24+
version = "1.2.0",
2525
description = "Multi-protocol package registry supporting Docker, Maven, npm, Cargo, PyPI, Go, Raw, RubyGems, Terraform, Ansible, NuGet, pub.dev, Conan, RPM, and Debian",
2626
license(name = "MIT"),
2727
contact(name = "The NORA Authors", url = "https://getnora.dev")

0 commit comments

Comments
 (0)