Skip to main content
Version: v5.7.0

Step artifacts

Included from Basic tier

Step artifacts are a run step's declared products, collected from the run workspace at the step's completion, before any per-step cleanup or run-end workspace sweep — cancelled and failed runs included — and stored past the workspace's deletion. They complement step outputs: outputs are control-plane metadata under a hard 64 KiB budget; artifacts are the transport for anything larger (reports, dist trees, bundles). Collected artifacts land in the same artifact store the OpsChain.artefact DSL and opschain artifacts commands use, so one retention policy, one audit event and one read surface cover both.

Declaring artifacts​

A run: step in an action file may carry an artifacts: list — and so may a composed uses: call site, whose declarations land on the composed action's last run: step (the one whose workspace holds what the action produced, such as a pack verb's build step); a built-in verb or an actions.rb# / mintpress# step takes none, and a call site may not redeclare an artifact the composed action already declares.

steps:
- name: publish_report
run: make report
artifacts:
- name: integration_report # the artifact code it is stored under
path: integration-report/** # glob, relative to the run workspace
required: true # default false
  • name is the stored artifact code: lowercase letters, numbers and underscores, at most 100 characters, unique within the step.
  • path is a doublestar glob resolved against the run workspace root: * stays within a path segment, ** crosses segments. Absolute paths and .. segments are refused when the file loads. Only regular files are collected (symlinks and directories are skipped), and every collection read — the matched files, an image declaration's descriptor and SBOM, step_result.json and the step files — opens through the run workspace itself: a path that resolves outside the workspace through a symbolic link is refused by name (resolves outside the run workspace) and nothing is stored, so a step can never hand the runner host's files to the server.
  • required: true fails the step when collection fails — no matching files, an over-budget set, or a storage failure — as an ordinary step failure (never system_error), so on_error: handling still applies. With the default required: false, a collection failure leaves a warn:artifacts:collection event on the step and the run continues.

A step that already failed (or was cancelled) is never re-failed over an artifact: collection still runs best-effort — a report produced before the failing assertion is usually exactly what you want to keep — and any failure is recorded as the warning event.

What is stored​

Each declaration's matched files are packed as one deterministic tar archive (sorted workspace-relative paths, fixed epoch timestamps, 0644 or 0755 modes), so the same file set digests identically on every lane. The record carries the run, step, name, file count, total size, blob digest and content type (application/x-tar); storing it emits the api:artifacts:create audit event.

  • Local lane (--local runs): the archive is stored in the local blob store with its artefacts row, and the files are additionally unpacked under <opschain home>/artifacts/<run-id>/<name>/… for direct use.
  • Server lane (runner-executed steps): the runner uploads the archive over the runner protocol (DataService.PutArtefact, protocol version 5) and the server stores it in the blobstore (blob_root). A runner whose protocol predates step artifacts never receives the declarations — the server withholds them loudly with a system line in the step's log.

Image artifacts (kind: image)​

An image build's product is not a file set but an image living in a registry, an AMI in an account or a version in a compute gallery. A kind: image declaration records what was built, where it is, and under which run and commit — the typed record promotion, approvals and audit act on (opschain docs image-builds). OpsChain is not the container registry: the record never carries the image's bytes, only its facts and, optionally, the SBOM, because the SBOM is evidence.

steps:
- name: build
run: ./scripts/build-and-push.sh # builds, pushes, writes the descriptor
artifacts:
- name: web_image
kind: image # a typed image record, not a file archive
path: .opschain/image/web_image.json # the descriptor the step writes (this is the default for the name)
sbom: sbom/web.spdx.json # optional: the only bytes the record carries
required: true

The descriptor file contract​

The step writes a JSON object at the declaration's path (default .opschain/image/<name>.json, relative to the run workspace) before it ends; the collector reads it at the step's completion:

FieldRequiredMeaning
identifieryesThe backend's image reference: the registry reference with tag (harbor.limebank.internal/web/web:dev-42), an AMI id, a gallery image version resource id.
digestyessha256:<hex> — the content digest the backend reports — or, for a backend without one (an AMI, a gallery version), the backend's unique resource id prefixed id: so the field is never empty.
providernoThe build-provider code; defaults to the step's IMAGE_BUILD_PROVIDER when the step dispatched with one.
registrynoThe registry code the image was pushed to; defaults to the step's IMAGE_REGISTRY. The identifier is relative to that record's host and repository.
detailnoA string map of backend facts: platform, base_image, size…

The step learns where the declaration expects the files: its environment carries OPSCHAIN_IMAGE_DESCRIPTOR (the first image declaration's descriptor path) and, when the declaration names one, OPSCHAIN_IMAGE_SBOM — both workspace-relative — so a verb composed under the declaration (buildkit:build, packer:build, opschain docs packs-common) writes where the collector reads.

A missing descriptor, a missing identifier or a digest in neither form is the declaration's collection failure (required: true fails the step as an ordinary failure; otherwise a warn:artifacts:collection event). The record is stamped with the producing run's sealed commit (source_commit), the run and the step, so provenance is on the record itself; when an sbom: file is declared it is packed as the record's archive (one file), otherwise the record holds no bytes. Retention treats the record as any artifact: a purge removes the SBOM bytes, the metadata row survives, so provenance outlives the evidence file.

Referencing an image: artifact://…@<digest>​

An image record is addressable by its node, code and digest:

artifact://<node path>/<code>@<digest>
artifact://projects/limebank/environments/dev/assets/web/web_image@sha256:9f3c…

The digest picks the exact version — never "the latest" — so a reference written into a promotion change names one build.

artifacts:resolve_image — the built-in that verifies a reference​

uses: artifacts:resolve_image is a platform built-in step (no runner, no tool, completed by the server like an emit: step) that verifies a reference and publishes the record's facts as its outputs:

action: deploy
inputs:
image: { required: true } # artifact://<node path>/<code>@<digest>
steps:
- name: pull_record
uses: artifacts:resolve_image # verifies the artifact exists, is an image, carries the digest, is readable
with:
image: ${{ inputs.image }}
- name: roll
run: scripts/deploy.sh "${{ steps.pull_record.outputs.identifier }}" "${{ steps.pull_record.outputs.digest }}"

The verb takes image: alone. It refuses by name when the reference is malformed, names an artifact the node does not hold, names a digest no stored version carries, names a record that is not an image, whose SBOM was purged, or whose node the run principal may not read — and, when the run target's effective core.image_provenance classification is required, when the record names no build provider or registry, carries no source commit, or its source commit is not the sealed commit of the run that produced it (the promotion-policy rule, opschain docs image-builds). On success the step's outputs carry identifier, digest, provider, registry, source_commit, run_id (the producing run), step_id, artifact_id, node_path, code, reference and one detail_<key> per detail entry, and the consuming run's chain records info:image_build:artifact_resolved naming the artifact it consumed — the link between the build and the deploy.

Reading image records back​

  • opschain artifacts list --kind image [--node <path>] lists the image records with identifier, digest, provider, registry and the producing commit (opschain docs cli/artifacts-list).
  • The v2 reads carry kind and image on every artifact row, and ArtifactService.ListArtifacts accepts filter.kind: image.
  • The GUI's artifact lists show the kind beside the code and the image's identifier and digest beneath it.

Size budgets​

Two budgets bound collection, both configured in config.yaml's artifacts: block (opschain docs config): artifacts.max_artifact_mb (default 512) meters one artifact's matched file bytes, and artifacts.max_run_mb (default 1024) meters a run's collected artifacts cumulatively (the server re-enforces both on the stored bytes at upload). An over-budget collection is refused with the budget named, under the required/optional semantics above. The local lane reads the same config.yaml when the home carries one, else the compiled defaults apply.

Reading artifacts back​

  • opschain runs artifacts <run-id> lists a run's artifacts newest-first (--local and against a server); on the local lane a workflow run lists the artifacts of every change it spawned.
  • opschain runs artifacts <run-id> --download <name> [--out <dir>] fetches the newest artifact stored under name: a collected artifact is unpacked under --out with its workspace-relative paths; a raw (DSL-stored) artifact is written as a single file.
  • The v2 API serves the same reads: NodeArtifactService.ListNodeArtifacts scoped by change_id, GetArtifact, and GetArtifactDownloadTarget for the content URL. Collected artifacts carry file_count.
  • opschain artifacts list|get|put remain the node-scoped surface.

Retention​

Collected step artifacts are artefact rows, so the existing retention.artifacts_days class covers them: expired artifacts are exported to the retention archive, tombstoned (downloads answer the purged error) and their blob bytes released once unreferenced — see opschain docs retention. The card asked for a retention.artifacts_days key on the existing machinery; that key already existed for the artifact store, and collected step artifacts deliberately join it rather than adding a parallel class. The local lane's unpacked convenience copies under <home>/artifacts/ are plain files on the developer's machine and are not swept by server retention.

⚠️ Artifacts bypass the log masker​

Step logs and outputs pass through the run's secret masker; collected artifact files do not — they are copied byte-for-byte from the workspace. A declared path must never select files carrying rendered secret values (a materialised properties file, a rendered credential, a vault export). Treat an artifact declaration as a publication decision: anyone who can read the run can download its artifacts. The release lane's release-artifacts/<version>/ tree (whose build-info.json carries the garble seed) is deliberately not declared for exactly this reason — custody material is staged to the operator-controlled absolute release.custody_dir path and moves through the vault runbook instead.

  • opschain docs storage — the blobstore the archives land in.
  • opschain docs retention — the artifacts_days class.
  • opschain docs config — the artifacts.* budget keys.
  • opschain docs events — api:artifacts:create and warn:artifacts:collection.
  • opschain docs orchestration — the action-file step schema.
  • opschain docs image-builds — the build providers and registries an image build dispatches with, and the worked examples.