Skip to content

Repository and Artifact Management

Context

CodeScoring.Save stores and serves artifacts for a development team: a builder publishes a package, and IDEs, CI agents, and engineers download it from the same URL — without leaving the company perimeter.

All artifacts live inside repositories, and repositories are grouped into projects. The same hierarchy is reflected in the web interface:

Project (e.g. backend-team)
└── Repository (e.g. maven-central)
    └── Artifact (e.g. commons-lang3:3.12.0)

A project is a container for the team's repositories. A repository is a concrete storage of one format (Maven, npm, Docker, etc.) and one of two types. An artifact is what lives inside a repository and what clients pull.

Two Repository Types

CodeScoring.Save supports only two repository types, and each addresses a different need.

Proxy repository — a caching proxy in front of an external source (for example, Maven Central or npmjs.com). When a client requests an artifact for the first time, Save fetches it from upstream, caches it, and serves it. All subsequent requests are served from the cache — this speeds up builds and reduces dependency on external services. Proxy repositories also benefit from OSA Proxy security policies (when configured), which can block downloads of unsafe components.

Hosted repository — internal storage where the team publishes its own artifacts. This is where internal libraries, vetted third-party components, and build artifacts go.

A single project may contain any number of repositories of either type.

URL Access Scheme

An external client (Maven, npm, Docker, etc.) reaches a repository through URLs of the form:

https://save.example.com/<format>/<project>/<repository>/<artifact-path>
Format Template
Maven https://save.example.com/maven/<project>/<repository>/<group>/<artifact>/<version>/<file>
npm https://save.example.com/npm/<project>/<repository>/<package>
NuGet https://save.example.com/nuget/<project>/<repository>/v3/index.json
PyPI https://save.example.com/pypi/<project>/<repository>/simple/
Go https://save.example.com/go/<project>/<repository> (used as GOPROXY)
Raw https://save.example.com/raw/<project>/<repository>/<filepath>
OCI / Docker https://save.example.com/v2/<project>/<repository>/<image>/...

For OCI / Docker, the standard /v2/ prefix is used as required by the OCI Distribution Spec. Nexus-compatible flat URL routing is also supported — see Working with OCI / Docker.

Web Interface Layout

The left-hand vertical navigation has the following sections:

  • Projects — the main section: a list of projects, with a list of repositories inside each project, and a tree of artifacts inside each repository;
  • Cleanup — automatic cleanup policies for unwanted artifacts;
  • Settings — accounts, roles, service accounts, configuration, and the audit log.

At the top of the interface is a global search — opened with the / key, it searches across projects, repositories, and artifacts simultaneously, with real-time suggestions.

Basic Scenario: Connect Your First Repository

Step 1. Create a Project

A project is a container for the repositories that will live inside it. It is convenient to split projects by team, product, or environment.

  1. In the sidebar, choose Projects.
  2. Click Create project in the top-right corner.
  3. Fill in the form:
    • Name — a human-readable project name (for example, Backend Team);
    • Color — a color that helps tell projects apart in lists;
    • Key — a URL-safe project identifier (for example, backend-team). If left empty, it is auto-generated from Name, even when Name does not use Latin characters. Key cannot be changed after creation;
    • Description — an optional project description;
    • Cleanup policy — a two-pane selector listing the existing cleanup policies. It can be left empty and assigned later.
  4. Click Create project.

After creation, the project page opens with an (empty) list of repositories and the project's metadata in the header.

Key vs. Name

Name is shown in the UI and can be changed at any time. Key participates in every URL and API path, so changing it would require reconfiguring every client.

Step 2. Create a Repository in the Project

A repository — concrete storage for artifacts of a single format — is created inside a project.

  1. Open the project you just created.
  2. Click Create repository in the top-right corner of the project page.
  3. Fill in the common fields:
    • Name — a human-readable name (for example, Maven Central (proxy));
    • Color — a color for visual distinction;
    • Key — a URL-safe identifier (for example, maven-central). It cannot be changed after creation;
    • Description — an optional description.
  4. Choose Format — one of the supported package formats: maven, npm, docker (OCI), nuget, pypi, go, raw. The format cannot be changed after creation.
  5. Choose Type:
    • Proxy — to proxy an external registry;
    • Hosted — to host your own artifacts.
  6. If Proxy is selected, additional fields appear:
    • Proxy URL — the upstream repository URL (for example, https://repo1.maven.org/maven2/);
    • Cache TTL, seconds — the metadata cache lifetime in seconds. For typical external registries, 86400 (one day) is enough.
  7. Optionally attach Cleanup policy — a list of cleanup policies that should run against this repository.
  8. Click Create repository.

After creation, the repository page opens. The header shows the status (Enabled/Disabled), the format, the type, the creation and update timestamps, and — for a proxy repository — a clickable Remote URL link to the upstream.

Changing the repository status

The status (Enabled/Disabled) is set on the edit form: open the repository, click Edit repository in the header, flip the top Status radio toggle, and click Save. A disabled repository does not accept or serve requests, but it is not deleted and keeps its artifacts.

Step 3. Get Ready-Made Snippets for Connecting Clients

In the right part of the repository header there is a chain-link icon button. Clicking it opens the Useful snippets popover — a set of ready-made configuration fragments and commands tailored to the repository format.

  1. Click the chain-link button in the repository header.
  2. In the card that appears, scroll through the snippets:
    • each snippet has a title (for example, settings.xml, .npmrc, pip.conf, docker login), a syntax-highlighted code block, and a short description;
    • the set of snippets is composed server-side based on the repository's format and type. For Maven, you typically see a <mirror> fragment for settings.xml and a <repository> fragment for pom.xml; for Docker — docker login and docker pull; for npm — a line for .npmrc; and so on.
  3. Click Copy to the right of the snippet you need — it goes straight to the clipboard.
  4. Paste the snippet into the client's configuration file, or run the command in a terminal.

Step 4. Publish and Download the First Artifact

From this point on, everything happens on the client side — Save accepts the standard requests from each package manager. For a hosted repository, the usual path is publishing via mvn deploy, npm publish, twine upload, or docker push. For a proxy repository, an ordinary pull is enough — Save will cache the artifact on the way through.

The first request may require authentication — see Connecting Clients and Authentication.

Once a client uploads or downloads something for the first time, the artifact appears in the tree on the repository page.

Working with Artifacts in a Repository

The repository page has two columns: the artifact tree with search and sort on the left, the details of the selected artifact on the right.

Browsing the Tree

The tree shows the structure of the repository — folders and files with icons. Files with a "lock" icon are protected (locked/release) artifacts that cannot be deleted or overwritten without extra steps. Folders are expanded lazily: children are fetched on click.

Clicking a file opens its details in the right pane: name, size, path, checksum, and format-specific metadata (Maven groupId/artifactId/version, Docker tags, and so on).

Search, Sort, and Filters

Above the tree is the Search artifacts field: type a substring and press Enter — a flat list of matching artifacts appears with context highlighting. Clicking a row opens the details of the matching artifact.

Two buttons sit next to the search field.

The Sort button controls how the tree is ordered:

  • Sort byName, Date added, or Date modified;
  • Sort directionA to Z or Z to A.

The filter button (funnel icon), available for proxy repositories, exposes a Show uncached artifacts toggle. By default the tree shows only what is already in the local cache. With the toggle enabled, Save also lists artifacts known to upstream but not yet cached — useful for browsing what's available in the proxied source without triggering a real pull. This is not available for every repository format, because the underlying protocols differ.

Actions on a Single Artifact

After selecting an artifact, the right pane offers:

  • Download — saves the file locally;
  • Lock artifact — marks the artifact as release. Once locked, the artifact cannot be deleted or overwritten by a normal upload — this protects released versions from being silently replaced;
  • Delete artifact — removes the file from the repository (for hosted) or from the cache (for proxy).

Lock is irreversible in the UI

The release flag can only be cleared through the API: PUT /api/v1/artifacts/release?id=<artifact-id> with {"is_release": false}.

Bulk Actions

Each tree row has a checkbox on its left. Selecting several files reveals an action bar at the top:

  • Lock — locks the selected artifacts in bulk;
  • Delete — deletes them in bulk.

Both operations are atomic for the entire selection.

Connecting Clients and Authentication

Save supports several authentication methods to cover both interactive clients and CI pipelines. The client chooses the method itself through the Authorization header (or a format-specific header).

Method Header cs-auth type When to use
Basic Auth (user) Authorization: Basic base64(<username>:<password>) basic Interactive work from IDEs, manual curl, mvn/pip/npm runs as a specific user
Basic Auth (robot account) Authorization: Basic base64(sa$<robot-name>:<api-key>) api_key CI/CD pipelines, build runners, and any service integration
Bearer JWT Authorization: Bearer <jwt> bearer_jwt Session requests from the web interface and other services after login via cs-auth
Bearer (opaque token) Authorization: Bearer <opaque-token> npm_token Any non-JWT bearer token; in practice, the npm client after npm adduser / npm login
X-NuGet-ApiKey X-NuGet-ApiKey: <api-key> nuget_key NuGet format only, for compatibility with dotnet nuget push
Docker v2 token Authorization: Bearer <docker-jwt> docker_token OCI clients after going through the 401 → retry → 200 cycle

A Bearer token with three dot-separated segments is classified as a JWT; any other Bearer token is treated as an opaque npm token. The sa$ prefix in Basic Auth marks a service account (robot).

Robot Accounts for CI/CD

Personal passwords are not recommended for CI/CD integrations — they're tied to an employee and rotate often. The right tool is a robot account: a service account with its own role and a long-lived API key.

Creating Through the Web Interface

  1. In the sidebar, open Settings -> Robot accounts.
  2. Click Create robot account in the top-right corner.
  3. Fill in the fields:
    • Login — the service name used for authentication (for example, ci-builder). Save automatically adds the service-account prefix to the value you enter;
    • Name — a human-readable name (for example, CI Builder Bot);
    • Description — an optional description;
    • Expires in — the date when the key expires. Quick presets Week, Month, and Year are available below the field. To make the key permanent, tick the Never checkbox to the right of the date picker — the date input is then disabled automatically;
    • Global permissions — the robot's global permissions;
    • Scoped permissions — permissions limited to one, several, or all projects or repositories. The Scope switch selects the level (Projects / Repositories), and each scope adds entries through Add project permissions / Add repository permissions.
  4. Click Create robot account.

When the robot is created successfully, a modal window with the API key opens automatically — this is the password used for Basic Auth.

The API Key Window

After clicking Create robot account, Save opens the API key window. The modal shows:

  • API key — a read-only field with the key itself and a Copy button that puts it on the clipboard;
  • Key prefix — a short prefix of the key. It lets you identify the key in the audit log without revealing the full secret;
  • Expires at — the expiration date (if one was set);
  • Warning — server-supplied text, for example Store this API key securely — it cannot be retrieved again.

Below the secret is an I have copied the key button that closes the window and takes you to the newly created robot account's detail page.

The API key is shown only once

After the window closes, the same key cannot be viewed or retrieved again — Save stores only its hash. Copy the key and save it into your CI secret storage (GitLab CI variables, GitHub Actions secrets, HashiCorp Vault) before closing the window. If the key is lost, rotate it (either through the robot account's edit form in the UI, or via POST /admin/robots/<id>/rotate-key) — once rotated, the old value becomes invalid.

Creating Through the API

curl -X POST https://save.example.com/api/v1/admin/robots \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-jwt>" \
  -d '{
    "username": "ci-builder",
    "display_name": "CI Builder Bot",
    "description": "Robot account for CI/CD pipelines",
    "permissions": {
      "project": {
        "backend-team": ["project_artifacts.download", "project_artifacts.upload"]
      }
    },
    "key_expires_in_seconds": 7776000
  }'

The response returns api_key, which is exposed only once at creation time — save it in your CI secret storage.

Client Usage

username = sa$<robot-name>, password = <api-key> — standard Basic Auth:

curl -u "sa\$ci-builder:<api-key>" https://save.example.com/...

Escaping $ in shell

In bash, $ inside single quotes is preserved as-is; inside double quotes it must be escaped as \$. No escaping is needed in YAML / TOML / XML configuration files.

NuGet API key — implementation detail

The X-NuGet-ApiKey header is supported directly: cs-auth classifies it as nuget_key and validates it by the 12-character key prefix. Both options work for NuGet, but we recommend Basic Auth with a robot account — it is consistent with the other formats, and the robot's identity is recorded explicitly in the audit log.

Anonymous Read Access

When the global AllowAnonymousRead flag is enabled, unauthenticated requests receive a PermissionSet granting projects.view, project_repos.view, project_artifacts.view, and project_artifacts.download across all projects. For Docker / OCI this mode still requires the 401 → /v2/token → 200 cycle — otherwise the Docker client cannot work (see Working with OCI / Docker).

The current value of the flag is visible under Settings -> Configuration. Changing it is done through the API:

curl -X PUT https://save.example.com/api/v1/admin/config \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-jwt>" \
  -d '{"key": "AllowAnonymousRead", "value": "true"}'

Cleanup Policies

To keep storage from growing without bound, repositories can have cleanup policies attached — rules for automatic deletion (or retention) of artifacts. Save supports five policy types; simple ones are enough for most cases, while complex AND/OR conditions use the expression type.

Creating a Policy

  1. In the sidebar, open Cleanup.
  2. Click Create policy.
  3. Fill in the common fields: Display name, Description, and optionally Schedule (a five-field cron expression: minutes, hours, day, month, weekday).
  4. Choose Policy type — this defines the main cleanup rule:
Policy type Purpose value parameter
delete-snapshots-older-than Delete SNAPSHOT/dev versions older than N days number of days, for example 30
keep-latest-versions Keep only the N latest versions of each artifact, delete the rest N, for example 5
delete-by-age Delete any artifact older than N days number of days
delete-by-size Delete the oldest artifacts once a size limit is exceeded size in gigabytes as an integer, for example 50
expression Flexible DSL rule combining criteria with AND/OR an expression object is passed instead of value
  1. Specify the Formats the policy applies to. Leaving this empty means it applies to every format.
  2. Tick the Active flag for the policy to start working immediately.
  3. For the expression type, build the criteria tree as well — each criterion combines a type, a comparison operator (eq, ne, gt, lt, matches, contains), and a value; criteria can be grouped with logical operators AND or OR.
  4. Click Create policy.

Available Criterion Types for expression

Full list: GET /api/v1/enums/cleanup-criterion-types.

Group type Purpose
Age created_before Artifact age (in days)
last_downloaded_days Days since the last download
not_downloaded_since Never downloaded, or not downloaded since the given date
Version version_pattern Glob pattern matched against the version
is_snapshot, is_prerelease, is_release Snapshot / pre-release / release markers
Name/path name_pattern, path_pattern Glob patterns
Size size_greater_than, size_less_than Artifact size
Docker docker_tag, docker_untagged Tag / no tag
Maven maven_classifier, maven_packaging Classifier / packaging
npm npm_scope Package scope
PyPI pypi_package_type sdist, bdist_wheel, …
NuGet nuget_is_prerelease NuGet pre-release flag
OCI oci_artifact_type OCI artifact type (Helm, Cosign, SBOM, …)
Retention keep_latest Keep N latest versions

Assigning a Policy to a Repository

The assignment happens on the create or edit form of a repository (or of a project — in which case the policy cascades to every repository inside the project).

  1. Open the repository and click Edit in the header.
  2. Scroll down to the Cleanup policy section.
  3. The left column (Available) lists every existing policy; the right column (Applied) lists the ones already attached. Move the policies you need between columns using the arrow.
  4. Save the changes.

Manual Run and Preview via API

# Dry run — see what would be deleted, without saving the policy
curl -X POST https://save.example.com/api/v1/cleanup/preview \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "repository_id": 42,
    "policy_type": "delete-snapshots-older-than",
    "value": "30"
  }'
# Run saved policies (with an optional dry_run)
curl -X POST "https://save.example.com/api/v1/cleanup/execute?project=backend-team" \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "policies": ["delete-old-snapshots", "keep-latest-5"],
    "dry_run": true
  }'

Access Management

Users, roles, and access rights for projects and repositories are managed by a separate service, cs-auth. Its API is proxied through Save under /api/v1/auth/* and /api/v1/admin/*. The authentication methods are described above in Connecting Clients and Authentication.

In the web interface, the relevant sections live under Settings:

  • Roles — role definitions with their permission sets;
  • Users — human accounts;
  • Robot accounts — service accounts (see above).

cs-auth API

The exact endpoints for managing users, roles, and project membership are part of the cs-auth API surface. Save forwards them transparently but does not describe them in its own Swagger.

Access Levels

Permissions in CodeScoring.Save are granular and follow the <resource>.<action> format. Roles in cs-auth are user-defined and consist of such permissions.

Repository level (artifacts inside a repository):

Permission Allows
artifacts.view Viewing the artifact list and metadata
artifacts.download Downloading artifacts
artifacts.upload Publishing artifacts (relevant for hosted)
artifacts.delete Deleting artifacts
artifacts.lock Lock/unlock one or all artifacts

Repository level (the repository itself):

Permission Allows
repos.view Viewing repository settings and statistics
repos.edit Changing repository settings
repos.delete Deleting the repository

Project level (cascade permissions that apply to every repository and artifact in the project):

Permission Allows
projects.view, projects.create, projects.edit, projects.delete Managing projects
project_repos.view, project_repos.create, project_repos.delete, project_repos.edit Managing all repositories in a project
project_artifacts.view, project_artifacts.upload, project_artifacts.download, project_artifacts.delete, project_artifacts.lock, project_artifacts.unlock All artifact operations in any repository of the project

Global (only available through a global-scope role):

Permission Allows
cleanup.view, cleanup.edit, cleanup.delete Managing cleanup policies
roles.create, roles.delete Managing roles
users.create, users.delete Managing users
config.view, config.manage Viewing and editing configuration
audit.view Viewing the audit log

Cascade logic

Project-scope permissions automatically cascade to the repository scope: a user with project_artifacts.download on a project gets artifacts.download on every repository inside that project. The * key grants wildcard access to all projects / repositories.

Assigning a User or Robot Account to a Project

Membership ties a user or a robot account to a project with a specific role. The assigned member receives every permission of the role and shows up as a member in the project header.

Through the Web Interface

Section in progress

The Members section on the project page is still being built. The backend supports every necessary operation (/api/v1/admin/projects/<project>/members), but the Members tab is not yet exposed in the project UI.

Until the tab is available, use the API operations below.

Through the API

Adding a member:

curl -X POST https://save.example.com/api/v1/admin/projects/backend-team/members \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-jwt>" \
  -d '{
    "user_id": 5,
    "role_id": 2
  }'

For human users role_id is required; for robot accounts (is_service=true) it can be omitted.

Listing members:

curl -u "<admin-username>:<admin-password>" \
  "https://save.example.com/api/v1/admin/projects/backend-team/members?limit=50"

Changing the role of an existing member:

curl -X PUT https://save.example.com/api/v1/admin/projects/backend-team/members/5 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-jwt>" \
  -d '{"role_id": 3}'

Removing a member from the project:

curl -X DELETE https://save.example.com/api/v1/admin/projects/backend-team/members/5 \
  -H "Authorization: Bearer <admin-jwt>"

The Same Actions via API

Every action covered above for the web interface is also available through the REST API. This is useful for bootstrap scripts, infrastructure-as-code (Terraform / Ansible), and integration tests.

Creating a Project

curl -X POST https://save.example.com/api/v1/projects \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "backend-team",
    "display_name": "Backend Team",
    "description": "Backend services and microservices",
    "color": "#4A90D9"
  }'

Creating a Proxy Repository

curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "backend-team",
    "name": "maven-central",
    "display_name": "Maven Central (proxy)",
    "format": "maven",
    "repository_type": "proxy",
    "remote_url": "https://repo1.maven.org/maven2/",
    "cache_ttl": 86400
  }'

Creating a Hosted Repository

curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "backend-team",
    "name": "internal-maven",
    "display_name": "Internal Maven",
    "format": "maven",
    "repository_type": "hosted"
  }'

Assigning a Cleanup Policy

curl -X POST "https://save.example.com/api/v1/cleanup/assignments?project=backend-team" \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "policies": [
      {"policy_name": "delete-old-snapshots", "priority": 10, "enabled": true}
    ]
  }'

Creating a Cleanup Policy

curl -X POST https://save.example.com/api/v1/cleanup/policies \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "delete-old-snapshots",
    "display_name": "Delete Old Maven Snapshots",
    "description": "Delete Maven SNAPSHOT versions older than 30 days",
    "policy_type": "delete-snapshots-older-than",
    "value": "30",
    "formats": ["maven"],
    "enabled": true,
    "schedule": "0 2 * * *"
  }'

Creating an Expression-Based Policy

curl -X POST https://save.example.com/api/v1/cleanup/policies \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "clean-old-prereleases",
    "display_name": "Clean Old Pre-releases",
    "policy_type": "expression",
    "formats": ["maven", "npm"],
    "expression": {
      "action": "delete",
      "logical_operator": "and",
      "criteria": [
        {"type": "is_prerelease", "value": "true"},
        {"type": "created_before", "value": "14"}
      ]
    },
    "enabled": true
  }'

Monitoring

Audit Log

The audit log records every change to projects, repositories, artifacts, roles, and robot accounts. In the UI it is available:

  • Globally — under Settings -> Audit log;
  • Per project — through the journal icon on the right of the project header, which opens the audit log filtered to that project.

Each entry contains the event time, the initiator, the resource type and identifier, the action type, and a set of fields with details (the name of the uploaded artifact, changed repository settings, and so on). Entries are grouped by day and displayed as a timeline, and the links inside an entry are clickable (they open the corresponding project, repository, policy, user, or role).

The log can be exported for a specific period through the Export button in the header (formats: JSON or HTML).

# Audit log for a specific repository via API
curl -u "<username>:<password>" \
  "https://save.example.com/api/v1/admin/audit?resource_type=repository&q=maven-central&limit=50"

Repository Statistics

Current aggregated repository metrics are available via the API:

curl https://save.example.com/api/v1/repos/<repository_id>/stats \
  -u "<username>:<password>"

Response:

{
  "id": 42,
  "name": "maven-central",
  "project_name": "backend-team",
  "artifact_count": 1523,
  "total_size": 16413032448,
  "cache_hit_ratio": 0.85,
  "last_activity": "2026-03-17T10:30:00Z"
}

Statistics fields

The endpoint returns aggregated values only. The cache_hit_ratio field is present only for proxy repositories.

Service Status

curl https://save.example.com/health

A global service health check. There are no per-repository health endpoints: upstream availability is checked by background workers and exposed through Prometheus metrics.

Logs

Logs are centralized and emitted as JSON. Filtering by a specific repository is done through standard log-aggregator tools.

Страница была полезна?