Artifacts

Overview

Artifacts are persistent files or directories produced by cikit workflow steps. They provide a common exchange mechanism between otherwise independent features.

A producer creates and registers an artifact:

producer
   |
   +-- file or directory
   |
   +-- env.copy_artifact(...)
           |
           +-- persistent output below PUBLIC_DIR
           +-- ArtifactRegistry metadata

A consumer can later query the registry:

ArtifactRegistry
   |
   +-- report generator
   +-- Sphinx preparation
   +-- later workflow step
   +-- project-specific logic

The consumer does not need to know where or how the artifact was originally created. It only needs the logical run, artifact type, or target name it is interested in.

Artifact Model

The Artifact object describes one registered artifact.

It contains six fields:

Field

Meaning

path

Absolute filesystem path of the registered file or directory.

target_name

Final file or directory name used to identify the artifact.

artifact_type

Semantic artifact category represented by ArtifactType.

run

Logical cikit run that owns the artifact.

size

File size in bytes. Directory artifacts use None.

hash

Stored artifact hash. File artifacts receive a SHA-256 hash automatically unless one is supplied explicitly.

Artifact.path is always absolute. Creating an Artifact with a relative path is rejected.

size and hash default to None when an Artifact is created without these metadata values.

The object is immutable after creation.

Artifact Types

Artifact types describe the purpose of an artifact independently from its file extension or physical source directory.

The current ArtifactType values are:

Type

Persisted value

Typical use

BADGE

badge

Generated status or metric badge.

TIMELINE

timeline

Rendered timeline chart.

XUNIT

xunit

XML result artifact used by test and coverage workflows.

LINT

lint

Machine-readable lint output.

RST

rst

Generated reStructuredText report page.

FOLDER

folder

Directory artifact such as generated HTML documentation or coverage.

METADATA

meta

Generated project metadata output.

EXTENSION

extension

Extension-related generated output.

LOG

log

Persisted command or build log.

BUILD

build

Direct build output such as a firmware image.

BUNDLE

bundle

Packaged archive containing one or more deliverables.

The string values are persisted in registry JSON files and are therefore part of the persistent artifact format.

Creating Artifacts

Artifact handling belongs to CiRunEnvironment. The base CiEnvironment does not manage artifacts because artifact ownership is tied to a logical run.

The normal producer API is copy_artifact():

from pathlib import Path

from cikit.artifacts import ArtifactType

artifact = env.copy_artifact(
    Path("build/firmware.bin"),
    ArtifactType.BUILD,
)

For a relative source path, cikit resolves the source below PROJECT_ROOT.

Absolute source paths may point to any accessible location, including temporary directories outside the project tree.

copy_artifact() performs four operations:

  1. Resolve and validate the source file or directory.

  2. Copy it into the persistent output area selected for the artifact type.

  3. Determine the artifact metadata.

  4. Register the copied artifact for the current logical run.

For regular files, cikit stores the file size and calculates a SHA-256 hash after the file has been copied.

If the producing tool already provides a SHA-256 hash, it can be supplied explicitly:

artifact = env.copy_artifact(
    Path("build/firmware.bin"),
    ArtifactType.BUILD,
    artifact_hash=firmware_hash,
)

The supplied hash is stored instead of calculating it again.

The returned object is the registered Artifact and therefore contains the resolved path together with its size and hash metadata:

print(artifact.path)
print(artifact.size)
print(artifact.hash)

Custom Target Names

A different destination name can be supplied with target_name:

artifact = env.copy_artifact(
    Path("build/firmware.bin"),
    ArtifactType.BUILD,
    target_name="controller.bin",
)

Nested destinations are also supported:

artifact = env.copy_artifact(
    Path("build/firmware.bin"),
    ArtifactType.BUILD,
    target_name="firmware/controller.bin",
)

The persistent file is then placed below the type-specific target directory using the supplied relative destination.

artifact.target_name represents the final file or directory name, while the complete registered path is available through artifact.path.

Directory Artifacts

copy_artifact() supports both files and directories.

For example, a complete generated HTML tree can be registered as one FOLDER artifact:

documentation = env.copy_artifact(
    Path("build/html"),
    ArtifactType.FOLDER,
)

Directories are copied recursively before being registered.

Folder artifacts do not have a file size, so artifact.size is None. No hash is calculated automatically for a folder, so artifact.hash also defaults to None.

An explicitly supplied SHA-256 artifact_hash is preserved even for a folder artifact.

Persistent Layout

CiRunEnvironment owns the mapping from artifact type to persistent destination below PUBLIC_DIR.

The current layout is:

public/
|-- artifacts/
|-- badges/
|-- cikit_data/
`-- rst/

BADGE artifacts are stored below:

public/badges/

RST artifacts are stored below:

public/rst/

All other currently supported artifact types are stored below:

public/artifacts/

The physical layout is intentionally owned by the environment layer. Producers and consumers should use the artifact API instead of constructing these paths themselves.

Artifact Registry

ArtifactRegistry persists artifact metadata locally.

CiRunEnvironment creates the registry with:

cache directory  = CIKIT_DATA_DIR
artifact root    = PUBLIC_DIR

One metadata file is stored per logical run:

CIKIT_DATA_DIR/
|-- build.artifacts.json
|-- test.artifacts.json
`-- docs.artifacts.json

The registry stores artifact paths relative to PUBLIC_DIR.

For example, an in-memory artifact may contain:

path = /workspace/project/public/artifacts/firmware.bin
target_name = firmware.bin
artifact_type = build
run = build
size = 524288
hash = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

The corresponding build.artifacts.json entry is stored as:

{
    "artifacts/firmware.bin": {
        "target_name": "firmware.bin",
        "type": "build",
        "size": 524288,
        "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
}

size and hash are part of the persisted artifact metadata. Folder artifacts normally store both values as null unless an explicit hash was supplied.

Persisting relative paths keeps artifact metadata portable across workspaces and CI jobs.

When registry entries are loaded, the relative path is resolved below the configured artifact root and returned as an absolute Artifact.path.

Paths that are absolute in persisted metadata or escape the configured artifact root are rejected.

Updating Registry Entries

Within one logical run, the persisted path is the registry key.

Registering another artifact for the same path updates the existing metadata entry rather than creating a duplicate entry.

Artifacts at different paths remain separate even if they share the same target_name or artifact type.

Querying Artifacts

CiRunEnvironment provides general and run-specific lookup methods.

All Locally Known Runs

Calling get_artifacts() without a run filter searches all locally available artifact registry files:

artifacts = env.get_artifacts()

A type filter can be supplied:

artifacts = env.get_artifacts(
    artifact_type=ArtifactType.BUILD,
)

Specific Run

Pass the logical run explicitly:

artifacts = env.get_artifacts(
    "build",
    ArtifactType.BUILD,
)

The run=None convention intentionally means all locally known runs.

Active Run

Most report and macro code operates on the current logical run and uses the convenience wrapper:

artifacts = env.get_run_artifacts(ArtifactType.TIMELINE)

This is equivalent to filtering get_artifacts() by env.name.

Getting One Artifact

get_artifact() resolves one uniquely identifiable artifact:

artifact = env.get_artifact(
    "firmware.bin",
    "build",
    ArtifactType.BUILD,
)

If no run is supplied, all runs are searched.

The lookup fails when no artifact matches or when the result is ambiguous.

For the active logical run, use:

artifact = env.get_run_artifact(
    "firmware.bin",
    ArtifactType.BUILD,
)

This avoids repeatedly passing env.name.

Producer and Consumer Decoupling

Artifact registration is the integration boundary between cikit features.

A producer only needs to register the artifact with the appropriate type:

timeline.create(env, "build")

Internally this eventually registers a TIMELINE artifact.

A consumer decides independently whether it supports that artifact type:

timelines = env.get_run_artifacts(ArtifactType.TIMELINE)

There is no global rule that automatically injects every artifact into every report or documentation page.

For example, a report that supports TIMELINE artifacts can include all timeline charts registered for its run. A report that does not query ArtifactType.TIMELINE remains unaffected by them.

The same principle applies to build outputs, bundles, logs, folders, and other artifact categories.

Report Integration

Reports may consume artifacts through the artifact registry. They do not receive files or producer objects directly.

Each report implementation decides which artifact types it supports and which artifacts of the active logical run it includes.

For example, a report can query timeline artifacts with:

timelines = env.get_run_artifacts(ArtifactType.TIMELINE)

Registering a TIMELINE artifact therefore does not automatically make it part of every report. It only becomes visible in reports that explicitly consume ArtifactType.TIMELINE.

Current Built-in Reports

The built-in report implementations currently consume artifacts as follows:

Report

Artifact types

Notes

Test report

TIMELINE

Includes timeline artifacts of the active run.

Build report

TIMELINE, BUILD, BUNDLE, LOG

Includes timeline charts, build artifacts and bundles with download links, file sizes, and SHA-256 hashes, plus command logs.

Coverage report

TIMELINE, FOLDER

Includes timeline artifacts and links to the registered HTML coverage directory.

Ruff report

TIMELINE

The main report includes timelines. Filtered reports intentionally do not.

Pylint report

TIMELINE

The main report includes timelines. Filtered reports intentionally do not.

This behavior is report-specific. Other report implementations may consume different artifact types or none at all.

Producer and Report Independence

Artifact producers do not need to know which reports will consume their outputs.

For example:

timeline = chart_timeline(
    env,
    keys=["warnings", "errors"],
    title="Build diagnostics",
    limit=20,
)

timeline.create(env, "build_diagnostics")

reports.write_build_report(env)

TimelineChart.create() registers the rendered SVG as a TIMELINE artifact. write_build_report() later discovers that artifact through the registry.

No TimelineChart object or SVG path is passed directly to the report.

The same pattern is used for build outputs, bundles, logs, coverage folders, and other artifact types:

producer
   |
   +-- register artifact
            |
            v
     ArtifactRegistry
            |
            +-- report supporting that artifact type

This keeps producers and report generators independent from each other.

Sphinx Integration

The Sphinx preparation step also consumes registered artifacts.

The current mapping into the Sphinx source tree is:

RST
   -> docs/_generated/

BADGE
   -> docs/_static/badges/

all other artifact types
   -> docs/_static/artifacts/

This mapping is based on artifact metadata rather than on producer-specific output paths.

For example, a registered TIMELINE artifact can be copied into the Sphinx static artifact area and later referenced through the cikit-chart directive.

The completed Sphinx HTML tree is itself registered as a FOLDER artifact.

Bundles

Bundles are ordinary artifacts with type BUNDLE.

The bundle macro creates an archive from a file or directory and then registers the generated archive through copy_artifact().

Supported bundle formats are:

  • .zip

  • .tar

  • .tar.gz

Because bundles use the normal artifact mechanism, consumers such as the build report can discover them through:

bundles = env.get_run_artifacts(ArtifactType.BUNDLE)

Artifact Handling and Publication

The artifact mechanism manages local persistent outputs and their run-related metadata. It does not itself publish files to a CI service.

Publication remains a responsibility of the surrounding CI configuration.

For example, a GitLab pipeline may publish files from public/ as CI artifacts or publish the registered Sphinx output through GitLab Pages.

This separation keeps artifact creation and discovery provider-independent.

Relationship to Cikit Data

Artifact metadata and cikit run data are separate concepts.

Both are associated with a logical run, but they store different information:

cikit data

artifact registry

Stores

Metrics and structured run values

File and directory metadata

Example

build_time = 4.2

firmware.bin as BUILD

Run-bound

Yes

Yes

Timeline source

Yes

No

Physical files

No

References persistent files or directories

CIKIT_DATA_DIR is used as the local storage location for both run data and artifact registry metadata, but the two mechanisms remain independent.

See Also