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:
| 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.
- In the sidebar, choose Projects.
- Click Create project in the top-right corner.
- 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.
- Name — a human-readable project name (for example,
- 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.
- Open the project you just created.
- Click Create repository in the top-right corner of the project page.
- 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.
- Name — a human-readable name (for example,
- Choose Format — one of the supported package formats:
maven,npm,docker(OCI),nuget,pypi,go,raw. The format cannot be changed after creation. - Choose Type:
- Proxy — to proxy an external registry;
- Hosted — to host your own artifacts.
- 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.
- Proxy URL — the upstream repository URL (for example,
- Optionally attach Cleanup policy — a list of cleanup policies that should run against this repository.
- 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.
- Click the chain-link button in the repository header.
- 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 forsettings.xmland a<repository>fragment forpom.xml; for Docker —docker loginanddocker pull; for npm — a line for.npmrc; and so on.
- each snippet has a title (for example,
- Click Copy to the right of the snippet you need — it goes straight to the clipboard.
- 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 by —
Name,Date added, orDate modified; - Sort direction —
A to ZorZ 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 normalupload— 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¶
- In the sidebar, open
Settings -> Robot accounts. - Click Create robot account in the top-right corner.
- 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, andYearare 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.
- Login — the service name used for authentication (for example,
- 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:
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¶
- In the sidebar, open Cleanup.
- Click Create policy.
- Fill in the common fields: Display name, Description, and optionally Schedule (a five-field cron expression: minutes, hours, day, month, weekday).
- 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 |
- Specify the Formats the policy applies to. Leaving this empty means it applies to every format.
- Tick the Active flag for the policy to start working immediately.
- For the
expressiontype, build the criteria tree as well — each criterion combines atype, a comparison operator (eq,ne,gt,lt,matches,contains), and a value; criteria can be grouped with logical operatorsANDorOR. - 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).
- Open the repository and click Edit in the header.
- Scroll down to the Cleanup policy section.
- 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.
- 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:
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¶
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.