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
nameis the stored artifact code: lowercase letters, numbers and underscores, at most 100 characters, unique within the step.pathis 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.jsonand 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: truefails the step when collection fails — no matching files, an over-budget set, or a storage failure — as an ordinary step failure (neversystem_error), soon_error:handling still applies. With the defaultrequired: false, a collection failure leaves awarn:artifacts:collectionevent 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 (
--localruns): 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:
| Field | Required | Meaning |
|---|---|---|
identifier | yes | The 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. |
digest | yes | sha256:<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. |
provider | no | The build-provider code; defaults to the step's IMAGE_BUILD_PROVIDER when the step dispatched with one. |
registry | no | The 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. |
detail | no | A 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
kindandimageon every artifact row, andArtifactService.ListArtifactsacceptsfilter.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 (--localand 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 undername: a collected artifact is unpacked under--outwith its workspace-relative paths; a raw (DSL-stored) artifact is written as a single file.- The v2 API serves the same reads:
NodeArtifactService.ListNodeArtifactsscoped bychange_id,GetArtifact, andGetArtifactDownloadTargetfor the content URL. Collected artifacts carryfile_count. opschain artifacts list|get|putremain 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.
Related pages
opschain docs storage— the blobstore the archives land in.opschain docs retention— theartifacts_daysclass.opschain docs config— theartifacts.*budget keys.opschain docs events—api:artifacts:createandwarn: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.