github artifact-keeper/artifact-keeper-web v1.11.1
Artifact Keeper Web 1.11.1

2 hours ago

Sponsors

Thank you to our sponsors for supporting ongoing development of Artifact Keeper.

Backers

Become a sponsor to support the project and get your name listed here.

Thank You

  • @der-eismann for reporting the empty Maven Files tab (#987), type-to-confirm dialogs that kept the previous name (#973) and the migration that stayed "Pending" with failed items (#886), and for the fixes for that migration status (#931) and the move to Node.js 24 (#932, #933).
  • @kakawait for reporting that the login page defaulted to a Local tab that could not succeed when local login is disabled (#899).
  • @lboclboc for reporting that uploads of 2 GiB or larger failed before any request was sent (#887).
  • @major-sam for reporting that switching to a remote instance ended on "Remote instance not found" (#829).
  • @groboclown for reporting the Gradle filter option that matched nothing (artifact-keeper#3425, #814).
  • @berat-samli for the fix that lets a remote repository's upstream URL be edited (#1002, #1005).
  • @garrettKelleyCyOne for the fix that puts the registry host and repository into the OCI and Helm install commands (#951, #952).
  • @damjanek for the RPM Repodata Depth controls (#906, #907).
  • @Spenhouet for the neutral white light theme (#967, #969).
  • @vvs9896 for the repository signing settings on the repository and Signing pages (#937, #935).

Added

  • Repository signing can be set up from the repository and the Signing pages (#937) - turning on metadata signing for a Debian, RPM, Alpine or Conda repository took raw calls to /api/v1/signing/repositories/{id}/config, and the Signing page listed keys without saying which repositories use them. The repository Settings tab now has a Repository Signing section for instance admins (local, staging and virtual repositories of those formats): pick a global or repository-scoped key, or create one of the type the format needs (GPG for Debian and RPM, RSA for Alpine and Conda), and choose metadata signing, package-signature verification and required signatures; a key of the wrong type is flagged and cannot be used for metadata signing. The Signing page lists each signable repository with the key bound to it, read from each repository's stored config. Trade-offs: the Signing page checks the first 200 repositories, one config request each, and says so when the instance has more; the section is hidden from repository admins who are not instance admins, because the backend only lets instance admins change signing config.
  • A neutral white light theme, "Light (neutral)" (#967) - the light theme uses a warm beige palette for the page, cards, sidebar and inputs, which on some screens reads as a tinted background, and there was no plain white alternative. The header theme button now opens a menu with Light, Light (neutral), Dark and System instead of flipping between light and dark. Light (neutral) uses white and neutral-gray surfaces, borders and muted text with the same primary, ring, destructive and chart colors; text pairs meet WCAG AA, which a unit test checks. The warm light theme stays the default and the choice is persisted by next-themes as before. "System" still follows the OS between the warm light theme and dark; there is no setting to make it pick the neutral one.
  • Independent YUM metadata roots within a Local RPM repository (#906, backend artifact-keeper#4216, artifact-keeper#4230) - RPM users previously had only repository-root metadata, so sibling build directories could not serve independent package sets. Repodata Depth controls in repository creation and Settings now mirror the backend's directory-based metadata roots, with matching upload-path guidance and native curl/YUM setup examples. Zero preserves existing behavior; explicit changes require positively confirmed backend support and server-advertised bounds. Editing is limited to eligible empty Local RPM repositories, including no deleted artifact history, curation, publications or virtual membership; these roots do not create separate repositories or permissions. Older backends keep depth-zero workflows but cannot enable this setting. Operator documentation (docs/rpm-repodata-depth.md) ships with the backend; upgrade every backend node before enabling a positive depth, since mixed fleets are unsupported.

Changed

  • The web image now runs on Node.js 24 (#932) - the UI was still built and served on Node.js 22 although 24 is the current LTS line. The production image enables UBI 9's nodejs:24 module stream instead of nodejs:22, the development image moves from node:22-alpine to node:24-alpine, and every CI job installs Node 24, so the image, local development and CI run the same major. No application code changed: Next.js 16, Vitest 4, Playwright and every locked dependency already support Node 24. The runtime stage is unchanged too, since the UBI package still ships node linked against /usr/lib64/libnode.so.*, which it copies with a glob. Operators need do nothing unless they build the image themselves on a mirror of the UBI 9 repositories, which must carry the nodejs:24 stream.

Fixed

  • Package install commands for OCI images and Helm charts now name this registry (#951) - the Packages page, package detail page and a repository's Packages tab showed docker pull my-image:1.0.0, which Docker resolves against Docker Hub, not this instance; oras pull had the same gap and Helm showed helm install my-chart --version 1.0.0 with no chart reference at all. getInstallCommand only ever received the package name, version and format, so it had no host or repository key to build a reference from. It now takes both, with the host taken from the browser's window.location.host as the Setup Guide already does, and emits docker pull <host>/<repo>/<name>:<version> for docker/podman/buildx, oras pull <host>/<repo>/<name>:<version> for oras/wasm_oci, helm install <name> oci://<host>/<repo>/<name> --version <version> for helm_oci, and helm install <name> <repo>/<name> --version <version> for helm. Trade-offs: the host is the one the UI was reached on, so an instance serving the registry on a different hostname than the web UI still shows the UI's host (the Setup Guide has the same assumption); and the classic helm command assumes the repository was added under its key, as the Setup Guide's helm repo add <repo> ... does. Other formats are unchanged.

  • A migration that finished with some failed items no longer shows as "Pending" (#886, backend artifact-keeper#3521) - a job whose transfers mostly succeeded but partly failed (e.g. 145129/145144 (15 failed)) sat at "Pending" with a provisional ~100%, offered the Start button, and never showed its Reconciliation Report. Since artifact-keeper#3521 the backend finishes such a job as completed_with_errors, but the web's list of known job statuses predated it and mapped every unknown status to pending, which also kept the job out of the terminal-status gate the report is fetched behind. The status is now recognised: it renders as a yellow "completed with errors" badge at 100%, the job counts as finished (report loads, Delete is offered, Start is not), and an unknown job status now logs a console warning instead of silently reading as pending. The Reconciliation Report now also lists the failed items it carries, each with its source path and error message, which is the list the report asked for; the backend puts at most 100 failed items in a report, so the list says when it is showing the first 100. The report's recommendations, which the backend sends as {repository, action} objects since artifact-keeper#3925, are rendered as their text instead of being handed to React as objects, which threw. Scope limits: the report's HTML download is still the backend's "not yet implemented" placeholder, and the job's item table still cannot be filtered to the failed items, because the backend ignores the items endpoint's status parameter, so a job with more than 100 failures shows only the first 100; both are left to follow-ups.

  • A remote repository's upstream URL can be changed from the Settings tab instead of only being displayed (#1002) - the tab tracked key, name, description and visibility, and showed the upstream URL as read-only text in Repository info, so repointing a Remote or rotating credentials embedded in its URL meant deleting and recreating the repository or calling PATCH by hand. The backend has accepted upstream_url on PATCH since artifact-keeper#4467. The read-only row is replaced by an editable field, shown for Remotes only, and leaving it blank keeps the stored URL so an unrelated save never rewrites it.

    When the stored URL carries embedded credentials the backend redacts them out of the response and reports their presence separately (artifact-keeper#4452), so the field starts empty with the redacted URL as its placeholder: pre-filling it would have written the redacted form back and silently dropped the credentials. Typing the redacted URL back in exactly counts as no change and is not sent; the trade-off this accepts is that any other URL typed without the credentials replaces the stored one and drops them, so rotating or keeping them means entering the full URL. Moving a Remote to another origin while separate upstream-auth credentials are configured is still refused by the backend with a 409; the backend's message for it named a REST call and ran past the toast's length cap, so the save toast now says to set Upstream Authentication to None in the Edit Repository dialog first. On a backend older than artifact-keeper#4467 (before 1.10.0) the field is accepted but ignored, and the old URL reappears after the save.

  • Switching to a remote instance works again (#829) - choosing another registered instance in the header switcher ended on /login with "Remote instance not found" and React error #418 in the console. Two web defects: every SDK request carried the remote-instance proxy prefix twice (/api/v1/instances/<id>/proxy/api/v1/instances/<id>/proxy/...), because the client's base URL and its request interceptor each added it, so the local backend relayed /api/v1/instances/<id>/... to the remote, which has no instance with that id; and the switcher label was rendered from localStorage on the first client render while the server had rendered "Local", a hydration mismatch. The prefix is now added in one place, the provider renders "Local" until hydration completes and then the stored instance, and requests that belong to the local session (login, logout, refresh, TOTP verify, SSO, and the /api/v1/instances registry) are never relayed, so a user whose local session expires while a remote is active can sign in again and an expired session is refreshed instead of failing. The relay is done by the local backend with the API key stored for the instance; the browser never calls the remote, so CORS on the remote is not involved (only the switcher's online dot fetches the remote's /health directly). The backend relay drops query strings, so filters and pagination on remote lists are ignored until artifact-keeper#4776 is fixed.

  • Chunked uploads of files 2 GiB or larger no longer fail before sending anything (#887) - uploading a file of 2 GiB or more stopped at once with "Upload failed: The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired." and no network traffic. Before creating the upload session the web computes the file's SHA-256 (the backend requires it and checks it on completion), and it did so by reading the whole file into one buffer for crypto.subtle.digest; browsers cannot read a file that large in one piece and reject the read with NotReadableError. The file is now hashed incrementally, 8 MiB at a time, with a streaming SHA-256, and the chunks were already sent as file.slice() blobs, so no step reads the whole file and memory use no longer grows with file size. Trade-off: the streaming hash runs in JavaScript rather than the browser's native digest, so the "hashing" phase before a large upload takes longer (roughly 100 MB/s, about half a minute for 3 GiB, measured in Node).

  • The visual-regression mask now hides the web and server version strings (#786) - e2e/visual-mask.css has always carried a [data-testid="app-version"] rule, but no element had that testid, so the versions in the sidebar header and in the admin Settings "Server Version" and "Web Version" rows were baked into the screenshot baselines and every release bump shifted them; a bump that changes the string's width (1.9.9 to 1.10.0) could fail the visual job on a release with no UI change. Both render sites now carry the testid, and the sidebar component screenshot applies the mask like the page screenshots already did. A unit test now fails if any exact [data-testid="..."] rule in the mask names a testid nothing in src/ sets. Known gap: the substring rules (*="timestamp", *="date", *="id", *="token-value", *="spinner") match none of the static testids in src/ today either; they are left in place and are not covered by that check.

  • The admin Downloads page shows proxy pull-through serves instead of failing to load (#960, backend artifact-keeper#4539) - since backend 1.11.0 GET /api/v1/admin/downloads (and its by-IP and by-user variants) also lists downloads served through Remote (proxy) repositories, which have no artifact row, so their artifact_id is null. The page validated artifact_id as a required string, so the first proxy row made the whole response "not match the expected shape" and the page showed "Download attribution unavailable". Rows are now read with the new source, repository_id and path fields: the Events table has a Source column (Hosted / Proxy), and the Artifact column shows the repository key and path, with the artifact id under it for hosted rows; a Source filter (hosted and proxy, hosted only, proxy only) is passed to the backend; and the By IP and By User views count a proxied path as one item, where they previously could not count proxy rows at all. Rows from an older backend, which carry only artifact_id, render as before. The types are defined in the web because the generated SDK's DownloadRecord predates the change. The dashboard is unchanged: none of its tiles reads total_downloads from /api/v1/admin/stats, which now counts hosted plus proxy downloads, so adding a downloads headline is left for a separate change.

  • OCI tag list: sizes stay on one line and long tag names no longer stretch the row (#957) - in a Docker repository's grouped view every size wrapped onto two lines (365.93 / MB), the "Last pushed" header wrapped too, and cosign signature tags (sha256-<64 hex>.sig, 71 characters) wrapped mid-token, making those rows three lines tall and pushing the digest column right. The size and date cells and their headers no longer wrap, and the tag name is cut to one line at 20rem with the full name on hover; the multi-arch badge keeps its width. This completes #957: the type filter and the Path column were fixed in 1.11.0. Signature tags are still listed as tags; showing them on the signed image instead is #956.

  • With local login disabled, the login page opens on the LDAP form instead of a "Local" tab that cannot succeed (#899) - on a deployment with an LDAP provider and ALLOW_LOCAL_ADMIN_LOGIN unset, the login page showed a "Local" tab next to the LDAP tab and selected it, so the first form a user saw was one the server rejects. The page already received auth.local_login_enabled from GET /api/v1/system/config but only used it to decide whether to show the form at all. The Local tab is now offered only when local login is enabled, first-time setup is pending, or the URL carries ?fallback=local (the admin break-glass route, where Local stays the default); otherwise the first LDAP provider is selected, and with a single LDAP provider the tab bar is dropped and the form reads "Sign in with your account". The tabs are now announced as tabs (role="tablist", aria-selected).

  • Type-to-confirm dialogs open empty after a delete (#973) - after deleting a repository, user, group or service account through a "Type <name> to confirm" dialog, the next delete on the same page opened with the previous name already typed in, and if that name matched the new target (delete foo, re-create it, delete it again) the Delete button was enabled with nothing typed. ConfirmDialog cleared its field only when Radix reported a user close (Cancel, Esc, click outside); the pages close the dialog themselves after a successful delete, which Radix does not report, and keep the dialog mounted. The dialog now clears the field whenever it opens, closes, or is pointed at a different target, so every caller gets the fix without changes.

  • The Repositories and Staging format filters no longer list a "Gradle" option that matches nothing (#814, reported as artifact-keeper#3425) - the "All formats" drop-down offered Maven and Gradle separately; Maven listed the Maven repositories and Gradle listed nothing, because Gradle builds use Maven repositories and almost no repository is created with format: "gradle". The backend already treats Gradle as the Maven handler (GET /api/v1/formats lists one Maven entry), but the web's filter was built from the same hardcoded list as the create dialog. The filter now leaves Gradle out; the create dialog still offers it, and the Maven set-me-up guide still carries the Gradle (Groovy and Kotlin) snippets. Trade-off: the list endpoint filters on one stored format value, so a repository that was created as gradle is reachable only under "All formats" rather than under Maven. SBT is not treated the same way, since the backend serves it with its own handler.

  • Admin-only system settings appear right after signing in with SSO or LDAP, and go away on sign-out, without a reload (#844) - GET /api/v1/system/config gives anonymous callers its public tier and admins extra fields (scanners, search and storage backends, permissions), and the web fetches it once at start-up with a 10-minute cache. The password and TOTP forms already refetched every query on sign-in (#487), but the SSO callback, LDAP login and demo auto-login did not, so an admin who signed in that way kept the anonymous payload until a hard reload. Sign-out had the opposite problem: it dropped the whole query cache, which detached the config query from the provider that renders it, so the admin payload stayed on screen after sign-out and the next sign-in in the same tab could not refresh it either. Every sign-in path now refetches the cache when the signed-in user changes, and sign-out still drops every other cached query but resets the system config in place, so the admin fields vanish at once and the anonymous tier is fetched. A profile refresh for the same user does not refetch. When the config request fails or its response does not match the expected shape, the console now says the built-in defaults are in use (which hide the SSO buttons), instead of the login page downgrading silently.

  • A package's Files tab lists its files for Maven and other formats whose name is not its folder (#987) - the Files tab passed the catalog package name to the tree endpoint as a path prefix, so a Maven package named com.example:demo looked for com.example:demo/... instead of com/example/demo/... and showed "No files found"; the package detail page also added a leading slash that matched nothing, and expanding any folder sent the node path with the repository key prepended, so subfolders came back empty. The prefix is now derived per format from how the backend lays out a hosted upload: Maven and Gradle turn the groupId's dots into folders; NuGet, Chocolatey and PowerShell use the lowercased id; Go escapes uppercase letters (!a); Docker and the other OCI formats look under v2/<image> (manifests only, since blobs have no artifact rows); Swift and VS Code turn scope.name into scope/name; Bazel and Protobuf look under modules/; Conan drops @user/channel. Other formats (npm including @scope/name, PyPI, Cargo, Helm, RubyGems, Composer and the rest) keep using the name, which already is the folder. Folder expansion now sends repository-relative paths. Trade-offs: rpm, debian, conda, alpine and pacman keep a package's files in a shared layout with no per-package folder, so their Files tab now says so and points to the Artifacts tab instead of showing an empty tree; remote (proxy) repositories still show no files, because cached files are not artifact rows the tree endpoint reads; a Maven or Conan package's tree shows every version, and a Conan name@user/channel package also shows the other user/channel variants of that name.

  • Bazel, pacman and rattler token-in-path conda clients work through the web origin (#996) - requests to /bazel/..., /pacman/... and /t/<TOKEN>/conda/... (the form pixi writes after auth login --conda-token) got the web UI's 404 page instead of reaching the backend, because the web proxy's list of format prefixes did not include them, the same gap #504 and #846 closed for /lxc and /general. The three prefixes are now forwarded. A unit test now compares the list with a copy of the backend's top-level format routes, so a format added to one without the other fails CI; the copy is maintained by hand, since the web build has no access to the backend source. The Helm chart's HTTPRoute has the same gap and is tracked separately (artifact-keeper/artifact-keeper-iac#374).

  • The permissions form says what a group target is, and is not (backend artifact-keeper#4653) - choosing principal type Service Account or User and target type Group creates a rule like service_account X -> group G -> read, which grants X the ability to view (read) or manage (admin) the group object. It does not add X to G and gives X none of G's repository access; membership is managed on the Groups page. The target label was just "Target Group", and a user read it as "give this account the group's access" (artifact-keeper#4593). The label now reads "Group (view / manage access)" and a hint under the picker says what the rule does and links to the Groups page for membership. Frontend only; the backend split between a group-target rule and user_group_members is correct and unchanged.

Notes

  • CI jobs that hang now fail within minutes instead of holding a PR for hours (#893) - the Unit Tests, Lint, Build and Dependency Audit jobs had no timeout-minutes, so they inherited GitHub's 6-hour default; #852's Unit Tests run hung for more than 30 minutes and had to be cancelled by hand, after which a re-run passed in about 3 minutes. Unit Tests (normally 3-4 minutes) and Build (1-2 minutes) are now capped at 15 minutes, Lint (about 1 minute) and Dependency Audit (under a minute) at 10, so a hang fails fast and can be re-run. The Docker image jobs and the E2E aggregate are left on the default for now. CI-only; nothing in the shipped image changes.
  • Anonymous downloads from a private or missing repository now get a 401 challenge, not a 404 (#999, backend artifact-keeper#4654) - since backend artifact-keeper#4655, GET /api/v1/repositories/{key}/download/{path} and /general/{key}/{path} answer a request that carries no credential with 401 and WWW-Authenticate: Basic realm="artifact-keeper" and Bearer realm="artifact-keeper" challenges, so curl/wget-style clients know to retry with credentials, and an API token is accepted as the Basic password there. The answer is identical for a private and a nonexistent repository, so the route still does not reveal which repositories exist; repository details, listings and the tree keep answering 404 to anonymous callers. The web needs no change; the anonymous-visibility E2E spec now expects the challenge and checks that private and nonexistent repositories get the same response.

What's Changed

  • fix(permissions): say what a group target grants, and that it is not membership (artifact-keeper#4653) by @brandonrc in #989
  • test(e2e): anonymous download on a private repository answers 401 with Basic and Bearer challenges (#999) by @brandonrc in #1000
  • fix(middleware): proxy /bazel, /pacman and /t to the backend (#996) by @brandonrc in #997
  • fix: OCI tag list layout, login tabs with local login disabled, proxy rows on admin Downloads (#957, #899, #960) by @brandonrc in #998
  • ci: give the Unit Tests, Lint, Build and Dependency Audit jobs timeouts by @brandonrc in #1011
  • chore(deps): vitest and @vitest/coverage-v8 5 together (supersedes #940, #943) by @brandonrc in #1013
  • fix(uploads): hash large files incrementally so 2 GiB+ chunked uploads start (#887) by @brandonrc in #1014
  • chore(deps): batch the open patch/minor dependabot bumps (#942, #944-#949, #975-#977) by @brandonrc in #1017
  • fix: 1.11.1 batch of six verified UI fixes (confirm dialogs, visual mask, Gradle filter, instance switcher, system-config refresh, Files tab) by @brandonrc in #1020
  • fix(repositories): allow editing a remote repo's upstream URL by @berat-samli in #1005
  • fix(migration): recognise the completed_with_errors job status by @der-eismann in #931
  • build(node): build and run the web image on Node.js 24 by @der-eismann in #933
  • fix(packages): include registry host and repository in OCI and Helm install commands by @garrettKelleyCyOne in #952
  • feat(repositories): add RPM Repodata Depth controls by @damjanek in #907
  • feat(theme): add a neutral white light theme (#967) by @Spenhouet in #969
  • feat(signing): repository signing settings on the repo and keys pages by @vvs9896 in #935
  • chore(release): web 1.11.1 prep: CHANGELOG section with Sponsors and Thank You, version bump by @brandonrc in #1032

New Contributors

Full Changelog: v1.11.0...v1.11.1

Don't miss a new artifact-keeper-web release

NewReleases is sending notifications on new releases.