Open-source universal artifact registry. Drop-in Artifactory/Nexus alternative with 40+ package formats, security scanning, WASM plugins, and edge replication. https://artifactkeeper.com
  • Rust 94.6%
  • Shell 4.7%
  • Go 0.4%
  • Python 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
knowinglyAnonymous 07a521fef2
feat(search): use OpenSearch for /search/quick when configured (#3678)
* feat(search): use OpenSearch for /search/quick when configured

OpenSearchService maintained an artifacts index that nothing ever read.
`search_artifacts` and `search_repositories` had no production callers; the
only references were source-introspection tests asserting the query was
written correctly without executing it. All search traffic went to the
PostgreSQL path, so setting OPENSEARCH_URL bought index writes, a cluster to
operate and a health dependency, and no query benefit.

This is not a performance change — the PostgreSQL search_vector + GIN index
from #2871 is fast. It is about what that path cannot do: typo tolerance,
relevance ranking with name/path boosts, and accurate deep-pagination totals.

Visibility is enforced on `repository_id` against the caller's AccessScope,
resolved from PostgreSQL per request, and NEVER on the index's own
`is_public` field. That flag goes stale: RepositoryService::update reindexes
only the repository document, not the artifact documents belonging to it, so
a repository flipped public -> private leaves its artifacts marked public
until the next full reindex, and delete orphans them still marked public.
Filtering on it would serve private artifacts to anonymous callers.

AccessScope is threaded through rather than Option<Vec<Uuid>> so "no
restriction" cannot be confused with "restricted to nothing", and an empty
allowlist short-circuits before querying rather than relying on how
OpenSearch treats an empty terms array. A test pins that the emitted filter
never mentions is_public.

Any OpenSearch error falls back to PostgreSQL with a warning, so a degraded
cluster cannot take search down. With OPENSEARCH_URL unset the handler takes
exactly today's path.

/search/advanced stays on PostgreSQL: its facets need aggregations.

Closes #3670

* test(search): cover the OpenSearch query path and document mapping

The coverage gate measured 43% (35/81) on new lines: the first round of
tests covered `visibility_filter_clause` in isolation but not the code that
builds and sends the query, nor the document -> API mapping.

Add nine tests, none needing a database:

opensearch_service, backed by wiremock so the assertions are against the
request actually put on the wire rather than the function's source text:
- an empty allowlist returns no results AND the mock server receives zero
  requests, pinning that deny-by-default does not depend on how OpenSearch
  treats an empty `terms` array
- a restricted scope puts `terms` on `repository_id` in the request body,
  and the body never mentions `is_public`
- admin scope emits no repository filter
- a 503 from the cluster surfaces as an error, so the handler's fallback has
  something to catch

search handler, for `build_search_result_item_from_doc`:
- every field maps across
- the doc and row mappings agree on `result_type`, since both feed the same
  response type and a divergence would be visible to clients
- a non-UUID document id degrades to the nil UUID instead of dropping the hit
- an out-of-range stored timestamp falls back to the epoch
- an absent version stays absent rather than becoming an empty string

Refs #3670

* fix(search): let PostgreSQL vet OpenSearch hits and bound the query

Two gaps in routing `/search/quick` through OpenSearch, both on the
request path this PR creates.

**PostgreSQL, not the index, decides which hits are real.** The `terms`
filter on `repository_id` is the right shape and the reasoning behind
not trusting the indexed `is_public` is right, but the index drifts from
PostgreSQL in more ways than repository visibility. Several soft-delete
paths mark `artifacts.is_deleted` with a direct `UPDATE` rather than
going through `ArtifactService::delete_artifact` — Helm chart deletes,
Maven and Conan version deletes — so `remove_artifact` never runs and the
document survives. The PostgreSQL path filters `a.is_deleted = false` on
every query; without a re-check the OpenSearch path is the one place a
deleted artifact reappears. So every hit is now vetted against
PostgreSQL before it is serialised: the row must exist, must not be
soft-deleted, and its repository must be in the caller's scope. One
indexed lookup over at most 50 ids, and the cluster's ranking order is
preserved. Re-checking the scope is redundant with the `terms` clause by
design — it is the gate, and it holds even if the mapping drifts off
`keyword`, the cluster truncates an oversized `terms` list, or the index
is restored from another deployment's snapshot.

**The fallback did not cover the worst failure.** `TransportBuilder` is
built without a request timeout, so "unreachable" only degraded
gracefully when the cluster failed fast. A cluster that accepts the
connection and never answers — a stop-the-world GC pause, a saturated
search thread pool, a blackholed route holding the socket open — would
hold the handler and its worker indefinitely. The query is now bounded at
three seconds, and a timeout falls back to PostgreSQL like any other
failure.

Four DB-backed tests drive the handler end to end through a wiremock
cluster: an artifact in a repository the caller holds no grant on is not
served, the same hit with a `read` grant is (so the first is about the
filter, not a dead path), a soft-deleted artifact is not served, and a
503 from the cluster degrades to PostgreSQL rather than 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6pg381RiaM9GsdVEyrN8M

---------

Co-authored-by: brandonrc <brandon.geraci@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 16:59:11 -05:00
.assets chore: versions (#2883) 2026-09-01 23:42:45 -05:00
.cargo fix(deps): bump quick-xml to 0.41 for XML-parser DoS hardening (#2160) 2026-07-02 14:31:31 +00:00
.claude/agents chore: add testing infrastructure, agent definitions, and test plan (#216) 2026-02-18 18:27:43 -06:00
.config fix(storage-gc)!: require dry_run and reject unknown fields on storage-GC requests (#3619) 2026-09-10 01:18:47 -05:00
.githooks chore(hooks): add native git hooks for fmt and pre-push checks (#1590) 2026-06-03 14:29:13 -05:00
.github fix(tests): pin the #1808 401 for an anonymous Conan request naming an unknown repository (#3846) 2026-09-12 13:36:07 -05:00
.sqlx fix(security): escape LIKE patterns that are assembled in Rust (#3557) (#3671) 2026-09-02 15:02:37 -05:00
.vex fix: address code scanning alerts #16 and #39 (#307) 2026-02-27 20:34:17 -06:00
backend feat(search): use OpenSearch for /search/quick when configured (#3678) 2026-09-12 16:59:11 -05:00
docker feat(docker): make backend and openscap images arbitrary-UID compatible (#3435) 2026-09-09 22:35:54 -05:00
docs feat(auth): enforce admin-configurable API token expiration policy (#3460) (#3625) 2026-09-10 00:36:01 -05:00
proof chore: versions (#2883) 2026-09-01 23:42:45 -05:00
scripts fix(security): make Swagger opt-in and stop advertising admin break-glass anonymously (#3842) 2026-09-12 02:45:10 -05:00
.clippy.toml fix(ci): pin the Rust toolchain and set a justified result_large_err threshold (#3552) 2026-08-24 16:25:31 -05:00
.dockerignore fix: keep scripts/openscap-wrapper.py in Docker build context (#559) 2026-03-23 15:44:52 -05:00
.env.example feat(opensearch): add OPENSEARCH_INDEX_PREFIX to namespace both indexes (#3677) 2026-09-12 12:37:23 -05:00
.env.local-dev feat(webhooks): v2 wire contract scaffolding (E1+E3+E6 combined) (#953) 2026-05-08 18:39:29 -05:00
.gitguardian.yaml feat(repositories): per-repository outbound egress proxy configuration (#3417) 2026-08-15 02:49:15 -05:00
.gitignore fix(ci): release-preflight tombstones + per-commit publish check, migration dedup gate, drop committed adapter binary, expire stale e2e soft-fail comments (#3364) 2026-08-14 14:27:50 -05:00
.jscpd.json fix(ci): stop the duplication gate silently skipping 45% of backend/src (#3523) 2026-08-23 14:29:04 -05:00
.mergify.yml ci: replace SonarCloud with native cargo-llvm-cov coverage gate (#776) 2026-04-17 16:45:45 -05:00
.trivyignore fix(security): rebuild grype from v0.118.0 and override grpc to v1.83.1 (#3696) 2026-09-03 15:42:16 -05:00
ARCHITECTURE.md fix(security): make Swagger opt-in and stop advertising admin break-glass anonymously (#3842) 2026-09-12 02:45:10 -05:00
Cargo.lock chore: bump ldap3 from 0.11.5 to 0.12.1 (#3802) 2026-09-11 19:07:41 -05:00
Cargo.toml chore(release): prepare 1.9.0 (#3768) 2026-09-08 08:35:06 -05:00
CHANGELOG.md feat(search): use OpenSearch for /search/quick when configured (#3678) 2026-09-12 16:59:11 -05:00
CLAUDE.md ci(tests): wire every test target into CI or exempt it explicitly, with a self-checking surface gate (#3620) 2026-09-09 19:42:00 -05:00
CODE_OF_CONDUCT.md chore: Add CONTRIBUTING.md, CODE_OF_CONDUCT.md, and clean up repo 2026-02-04 09:10:13 -06:00
CONTRIBUTING.md security: escape apostrophe in pypi html_escape helper (#1981) 2026-06-22 22:21:07 -05:00
docker-compose.concurrency-e2e.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.e2e-syspkg.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.local-dev.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.mesh-e2e.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.s3-maven-test.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.test.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
docker-compose.yml chore: versions (#2883) 2026-09-01 23:42:45 -05:00
lcov.info feat(repositories): allow custom User-Agent on remote repository outbound requests (#2452) 2026-07-12 20:07:27 -05:00
LICENSE chore: Change license from Apache-2.0 to MIT 2026-01-30 15:32:24 -06:00
README.md feat(formats): add jupyter as a PyPI alias format (JupyterLab Extensions) (#3786) 2026-09-09 11:17:12 -05:00
REDTEAM.md feat: Add red team security testing suite (#52) 2026-02-06 21:04:09 -06:00
RELEASING.md feat(release): certify release/X.Y.x commits from main, without widening the signing identity (#3821) 2026-09-10 13:26:33 -05:00
rust-toolchain.toml fix(ci): pin the Rust toolchain and set a justified result_large_err threshold (#3552) 2026-08-24 16:25:31 -05:00
rustfmt.toml feat: implement indiana-jones artifact registry backend 2026-01-15 12:29:33 -05:00
SECURITY.md ci: sign, attest and SBOM the release binaries, and verify before publishing (#3561) 2026-08-25 09:31:52 -05:00
SETUP-CI.md chore: Add launch epic, CI setup guide, and housekeeping checklist 2026-02-03 14:39:58 -06:00
TESTING.md chore: versions (#2883) 2026-09-01 23:42:45 -05:00

Artifact Keeper

CI Quality Gate Security Rating Vulnerabilities Lines of Code License Rust Docker Sponsor

An enterprise-grade, open-source artifact registry supporting 45+ package formats. Built with Rust.

Documentation | Demo | Website

Highlights

  • 45+ Package Formats - Native protocol support for Maven, PyPI, NPM, Docker/OCI, Cargo, Go, Helm, and 38 more
  • WASM Plugin System - Extend with custom format handlers via WebAssembly (WIT-based, Wasmtime runtime)
  • Security Scanning - Automated vulnerability detection with Trivy and Grype, policy engine, quarantine workflow
  • Hardened Containers - All images built on DISA STIG-approved Red Hat UBI 9 base images, non-root execution, no shell or package manager in runtime
  • Borg Replication - Recursive peer mesh with swarm-based artifact distribution and P2P transfers
  • Full-Text Search - OpenSearch-powered search across all repositories and artifacts
  • Multi-Auth - JWT, OpenID Connect, LDAP, SAML 2.0, and API token support
  • Artifactory Migration - Built-in tooling to migrate repositories, artifacts, and permissions from JFrog Artifactory
  • Artifact Signing - GPG and RSA signing integrated into Debian, RPM, Alpine, and Conda handlers

System Architecture

graph LR
    Client["CLI / Package Manager / Frontend"]
    Backend["Backend<br/>Rust · Axum<br/>45+ format handlers"]
    DB[(PostgreSQL 16)]
    Storage["Storage<br/>Filesystem / S3"]
    Meili["OpenSearch<br/>Full-text search"]
    Trivy["Trivy<br/>Container & FS scanning"]
    Grype["Grype<br/>Dependency scanning"]
    OpenSCAP["OpenSCAP<br/>Compliance scanning"]
    Peer1["Peer Instance"]
    Peer2["Peer Instance"]

    Client --> Backend
    Backend --> DB
    Backend --> Storage
    Backend --> Meili
    Backend --> Trivy
    Backend --> Grype
    Backend --> OpenSCAP
    Backend <-->|Borg Replication| Peer1
    Backend <-->|Borg Replication| Peer2
    Peer1 <-->|P2P Mesh| Peer2

Backend Architecture

The backend follows a layered architecture with a middleware pipeline processing every request.

flowchart TD
    REQ["HTTP Request"] --> MW["Middleware Pipeline"]

    subgraph MW["Middleware"]
        direction LR
        CORS["CORS"] --> AUTH["Auth<br/>JWT · OIDC · LDAP<br/>SAML · API Key"]
        AUTH --> RL["Rate Limiter"]
        RL --> TRACE["Tracing<br/>+ Metrics"]
        TRACE --> DEMO["Demo Mode<br/>Guard"]
    end

    MW --> ROUTER["Router<br/>50+ route groups"]

    subgraph HANDLERS["Handler Layer"]
        FMT["Format Handlers<br/>Maven · PyPI · NPM<br/>Docker · 41 more"]
        CORE["Core Handlers<br/>Repos · Artifacts<br/>Users · Auth"]
        ADV["Advanced Handlers<br/>Security · Plugins<br/>Peers · Migration"]
    end

    ROUTER --> HANDLERS

    subgraph SERVICES["Service Layer"]
        direction LR
        ART["Artifact<br/>Service"]
        REPO["Repository<br/>Service"]
        SCAN["Scanner<br/>Service"]
        PLUG["Plugin<br/>Service"]
        SEARCH["Search<br/>Service"]
    end

    HANDLERS --> SERVICES

    subgraph DATA["Data Layer"]
        direction LR
        PG[(PostgreSQL)]
        FS["Storage<br/>FS / S3"]
        MS["OpenSearch"]
        SC["Trivy / Grype / OpenSCAP"]
    end

    SERVICES --> DATA

Supported Package Formats

45+ formats organized by ecosystem. Each has a native protocol handler that speaks the package manager's wire protocol.

Languages & Runtimes

Format Aliases Ecosystem
Maven Gradle Java, Kotlin, Scala
NPM Yarn, Bower, pnpm JavaScript, TypeScript
PyPI Poetry, Conda, JupyterLab Extensions Python
NuGet Chocolatey, PowerShell .NET, C#
Cargo Rust
Go Go modules
RubyGems Ruby
Hex Elixir, Erlang
Composer PHP
Pub Dart, Flutter
CocoaPods iOS, macOS
Swift Swift Package Manager
CRAN R
SBT Ivy Scala, Java

Containers & Infrastructure

Format Aliases Ecosystem
Docker / OCI Podman, Buildx, ORAS, WASM OCI, Helm OCI Container images
Helm Kubernetes charts
Terraform OpenTofu Infrastructure modules
Vagrant VM boxes

System Packages

Format Ecosystem
RPM RHEL, Fedora, CentOS
Debian Ubuntu, Debian
Alpine Alpine Linux (APK)
Conda Conda channels
OPKG OpenWrt, embedded Linux

Configuration Management

Format Ecosystem
Chef Chef Supermarket
Puppet Puppet Forge
Ansible Ansible Galaxy

ML / AI

Format Ecosystem
HuggingFace Models, datasets
ML Model Generic ML artifacts

Editor Extensions

Format Aliases Ecosystem
VS Code Extension marketplace (VS Code, Cursor, Windsurf, Kiro)
JetBrains Plugin repository

Schemas

Format Ecosystem
Protobuf / BSR Buf Schema Registry, Connect RPC

Other

Format Ecosystem
Conan C, C++
Git LFS Large file storage
Bazel Bazel modules
P2 Eclipse plugins
Generic Any file type

Custom formats can be added via the WASM plugin system.

Security Scanning Pipeline

Every artifact upload is automatically scanned for known vulnerabilities.

flowchart LR
    UP["Artifact<br/>Upload"] --> HASH{"SHA-256<br/>Dedup"}
    HASH -->|New artifact| T["Trivy<br/>FS Scanner"]
    HASH -->|New artifact| G["Grype<br/>Dependency Scanner"]
    HASH -->|Already scanned| CACHE["Cached<br/>Results"]
    T --> SCORE["Vulnerability<br/>Score A-F"]
    G --> SCORE
    CACHE --> SCORE
    SCORE --> POL{"Policy<br/>Engine"}
    POL -->|Pass| OK["Stored"]
    POL -->|Fail| Q["Quarantined"]
  • Dual scanner - Trivy for filesystem/container analysis, Grype for dependency trees
  • Scoring - A through F grades based on finding severity and count
  • Policies - Configurable rules that block or quarantine artifacts
  • Signing - GPG/RSA signing for Debian, RPM, Alpine, and Conda packages

Container-image scanning (TRIVY_ADAPTER_URL). The base docker-compose.yml wires TRIVY_URL for the legacy trivy server (filesystem / incus rootfs scanning) only. To get first-class Trivy container-image reports, set TRIVY_ADAPTER_URL (e.g. http://scanner-adapter:8080) so the backend registers the dedicated ImageScanner against the in-repo scanner-adapter (Harbor Pluggable Scanner API). Without it, images are still covered by grype (registry mode) but no Trivy image report is produced. Uncomment the TRIVY_ADAPTER_URL line and the scanner-adapter service in docker-compose.yml to enable it.

The adapter pulls images back from the registry, so it must be told an address of the backend that is reachable from the adapter containerlocalhost is not, since the adapter itself listens on :8080 in its own network namespace (#3169). When nothing is configured the backend now advertises its own container address automatically; set TRIVY_ADAPTER_REGISTRY_URL (or the shared AK_GRYPE_REGISTRY_HOST) to override, e.g. http://backend:8080. The automatic derivation is skipped when the adapter's own URL is a loopback address, because that normally means the adapter shares this network namespace (cargo run next to a local adapter, or network_mode: host). It is not skipped correctly for an adapter container published on loopback (docker run -p 127.0.0.1:8081:8080 …scanner-adapter): there the backend reaches the adapter over loopback but the adapter cannot reach the backend that way, so set TRIVY_ADAPTER_REGISTRY_URL explicitly. The backend logs which fallback it took on every scan. Give the adapter its own trivy cache volume — reusing the trivy server's root-owned trivy_cache fails with permission denied on fanal/fanal.db because the adapter runs unprivileged (UID 1001).

Not-applicable scanners. A scanner that does not apply to an artifact's format (e.g. the filesystem/incus/openscap scanners on a Docker image) records a not_applicable result — a benign terminal state, distinct from failed. The scan-list API folds multiple not_applicable results for the same artifact into a single summary row (collapsed_not_applicable_count + collapsed_scan_types) so they read as one muted "not applicable" indication rather than N failures.

Borg Replication

Recursive peer-to-peer replication where every node is a full Artifact Keeper instance. No thin caches — each peer runs the same stack and can serve as an origin for other peers.

graph TD
    P1["Peer<br/>US-West"]
    P2["Peer<br/>EU-Central"]
    P3["Peer<br/>AP-Southeast"]
    P4["Peer<br/>US-East"]

    P1 <-->|"Chunked Transfer"| P2
    P1 <-->|"Chunked Transfer"| P4
    P2 <-->|"Chunked Transfer"| P3
    P3 <-->|"Chunked Transfer"| P4
    P1 <-->|"P2P Mesh"| P3
    P2 <-->|"P2P Mesh"| P4
  • Recursive peers - Every peer is a full instance (backend, DB, storage) that can originate replication to other peers
  • Swarm-based distribution - Artifacts replicate across the mesh based on demand
  • Chunked transfers - Large artifacts split for reliable delivery over unstable links
  • Network-aware scheduling - Bandwidth and latency profiling for optimal routing

WASM Plugin System

Extend Artifact Keeper with custom format handlers compiled to WebAssembly.

  • WIT-based interface - Plugins implement a well-defined FormatHandler contract
  • Wasmtime runtime - Sandboxed execution with fuel-based CPU limits and memory caps
  • Hot reload - Install, enable, disable, and reload plugins without restart
  • Sources - Load from Git repositories or ZIP uploads

Quick Start

Get running in 5 minutes with Docker Compose: Quickstart Guide

Documentation

Project Structure

artifact-keeper/
├── backend/          # Rust backend (Axum, SQLx, 6,400+ unit tests)
│   ├── src/
│   │   ├── api/      # Handlers, middleware, routes
│   │   ├── formats/  # 45+ format handler implementations
│   │   ├── services/ # Business logic (68 services)
│   │   ├── models/   # Data models (21 types)
│   │   └── storage/  # FS and S3 backends
│   └── migrations/   # 69 PostgreSQL migrations
├── edge/             # Peer replication service (Rust)
├── scripts/          # Test runners, native client tests, stress tests
└── .github/          # CI/CD workflows

Technology Choices

Layer Choice Why
Backend language Rust Memory safety, performance, strong type system
Web framework Axum Tower middleware ecosystem, async-first
Database PostgreSQL 16 JSONB for metadata, mature ecosystem
Search OpenSearch Fast full-text search, easy to operate
Security scanning Trivy + Grype + OpenSCAP Complementary coverage, industry standard
Plugin runtime Wasmtime Sandboxed, portable, WIT contract system
Storage Filesystem / S3 Simple default, cloud-ready upgrade path

CI/CD Pipeline

Seven GitHub Actions workflows handle testing, publishing, and deployment.

flowchart TD
    subgraph TRIGGER["Triggers"]
        PUSH["Push / PR<br/>to main"]
        TAG["Tag v*"]
        CRON["Daily 2 AM UTC"]
        SITE_PUSH["Push to site/**"]
    end

    subgraph CI["ci.yml — Every Push/PR"]
        direction TB
        LINT["🦀 Lint Rust<br/>fmt + clippy"]
        UNIT["🧪 Unit Tests<br/>cargo test --lib"]
        INTEG["🔗 Integration Tests<br/>+ PostgreSQL<br/>(main push only)"]
        SMOKE["🔥 Smoke E2E<br/>PyPI · npm · Cargo<br/>docker-compose.test.yml"]
        AUDIT["🔒 Security Audit<br/>cargo audit"]
        CI_OK["✅ CI Complete"]

        LINT --> UNIT
        LINT --> INTEG
        UNIT --> SMOKE
        SMOKE --> CI_OK
        AUDIT --> CI_OK
    end

    subgraph DOCKER["docker-publish.yml — Push to main / tags"]
        direction TB
        BE_BUILD["Backend<br/>amd64 + arm64"]
        OS_BUILD["OpenSCAP<br/>amd64 + arm64"]
        BE_MERGE["Multi-Arch<br/>Manifest"]
        OS_MERGE["Multi-Arch<br/>Manifest"]

        BE_BUILD --> BE_MERGE
        OS_BUILD --> OS_MERGE
    end

    subgraph E2E["e2e.yml — Manual / called by release"]
        direction TB
        PKI["🔐 Setup PKI<br/>TLS + GPG"]
        NATIVE["📦 Native Client Tests<br/>10 formats"]
        STRESS["🔥 Stress Tests<br/>100 concurrent uploads"]
        FAILURE["💥 Failure Tests<br/>crash · db · storage"]

        PKI --> NATIVE
        NATIVE --> STRESS
        NATIVE --> FAILURE
    end

    subgraph RELEASE["release.yml — Tags v*"]
        direction TB
        E2E_GATE["🚦 E2E Gate<br/>all formats + stress + failure"]
        BINARIES["📦 Build Binaries<br/>linux + macOS<br/>amd64 + arm64"]
        GH_RELEASE["🚀 GitHub Release<br/>binaries + checksums"]

        E2E_GATE --> BINARIES
        BINARIES --> GH_RELEASE
    end

    subgraph NIGHTLY["scheduled-tests.yml — Daily"]
        direction TB
        NIGHTLY_E2E["🌙 Nightly Smoke E2E"]
        DEP_CHECK["🔍 Dependency Check"]
        SEC_SCAN["🔒 Security Scan"]
    end

    subgraph SITE["site.yml"]
        PAGES["📄 Build + Deploy<br/>GitHub Pages"]
    end

    subgraph AMI["ami-build.yml"]
        PACKER["🖥️ Packer Build AMI"]
    end

    PUSH --> CI
    PUSH --> DOCKER
    TAG --> RELEASE
    TAG --> DOCKER
    CRON --> NIGHTLY
    SITE_PUSH --> SITE
    GH_RELEASE -.->|"on release published"| AMI

    classDef trigger fill:#6f42c1,color:#fff,stroke:#6f42c1
    classDef ci fill:#2ea44f,color:#fff,stroke:#2ea44f
    classDef docker fill:#0969da,color:#fff,stroke:#0969da
    classDef release fill:#d97706,color:#fff,stroke:#d97706

    class PUSH,TAG,CRON,SITE_PUSH trigger
    class LINT,UNIT,INTEG,SMOKE,AUDIT,CI_OK ci
    class BE_BUILD,OS_BUILD,BE_MERGE,OS_MERGE docker
    class E2E_GATE,BINARIES,GH_RELEASE release
Workflow Trigger What It Does
ci.yml Every push/PR Lint, unit tests, integration tests, smoke E2E (PyPI, npm, Cargo)
docker-publish.yml Push to main, tags Multi-arch Docker images (backend + OpenSCAP) to ghcr.io
e2e.yml Manual or called by release Full E2E: 10 native client formats, stress, failure injection
release.yml Tags v* E2E gate, cross-platform binaries, GitHub Release
scheduled-tests.yml Daily 2 AM UTC Nightly smoke E2E, dependency check, security scan
site.yml Push to site/** Build and deploy docs to GitHub Pages
ami-build.yml On release published Bake AWS AMI with Packer

Sponsors

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

Backers


Ash A.

Gabriel Rodriguez

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

Contributing

We welcome contributions! See CONTRIBUTING.md for guidelines.

Have questions or ideas? Join the conversation in GitHub Discussions.

License

MIT License - see LICENSE for details.


Built with Rust. "JFrog" and "Artifactory" are trademarks of JFrog Ltd. Artifact Keeper is not affiliated with or endorsed by JFrog.