|
1 | 1 | # Changelog |
2 | 2 | ## [Unreleased] |
3 | 3 |
|
| 4 | +## [1.2.0] - 2026-08-23 |
| 5 | + |
4 | 6 | ### 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). |
8 | 11 |
|
9 | 12 | ### 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). |
12 | 34 |
|
13 | 35 | ## [1.1.0] - 2026-07-26 |
14 | 36 |
|
|
0 commit comments