A CI job can finish successfully while the next job receives the wrong files. An artifact is a stored output handed to another step, job, person, or release process. Its name alone rarely identifies everything the consumer needs to know. Treat the handoff as a contract covering the producing run, source revision, payload, integrity evidence, access policy, and availability window. Those details turn “download the build” into a checkable operation.
Distinguish transfer from retention
An artifact can contain a binary, test report, coverage data, or a package that later becomes a release input. It is not inherently excluded from the release path. A dependency cache serves a different purpose: speeding up work that can be recreated. Decide what must remain available for approval, diagnosis, or rollback before selecting storage and retention. Rebuilding an expired output may create different bytes and requires its own validation.
GitHub Actions allows custom artifact retention subject to repository, organization, or enterprise limits. Plan around the actual configured window, including weekends and delayed approvals. A requested retention value cannot override the platform’s upper limit. If an artifact must outlive that window, use the project’s durable release or object storage with an explicit lifecycle. Do not describe a short-lived job store as an indefinite release archive.
When it breaks
Downloading a convenient “latest successful” run can pick a different commit, branch, or workflow. Resolve the intended producer explicitly and check the relationship between its source and the release being approved. The consumer also needs a dependency on successful production of the required files. An empty upload or missing artifact must not silently trigger deployment of whatever happens to be present in a workspace.
Integrity and provenance answer different questions. A checksum can detect a changed payload, but an attacker able to replace both the file and its checksum can create a consistent pair. An attestation needs verification of its subject, signer or workflow identity, and the relevant policy. Merely finding an attestation beside a binary is not evidence that the consumer validated it.
Access depends on the platform, repository visibility, and granted permissions; it is not universally equivalent to seeing a workflow. Still, package only the files needed for the handoff. Credentials, environment dumps, private headers, and unfiltered logs can become lasting copies when uploaded. Review both the member list and content before storing an artifact, particularly when build input comes from an untrusted contribution.
Check it quickly
Save the script as artifacts.py and run python3 artifacts.py. It uses only the Python standard library. The producer creates a temporary ZIP containing app.txt and a manifest with a local run identifier, a synthetic source digest, and the payload digest. The consumer reads only those named members and compares the expected producer before returning the payload.
from hashlib import sha256
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import json
payload = b'local build output\n'
source_digest = sha256(b'example source revision\n').hexdigest()
manifest = {'producer_run': 'local-run-41', 'source_sha256': source_digest,
'files': {'app.txt': sha256(payload).hexdigest()}}
def produce(path, data):
with ZipFile(path, 'w') as archive:
archive.writestr('manifest.json', json.dumps(manifest, sort_keys=True))
archive.writestr('app.txt', data)
def consume(path, expected_run):
with ZipFile(path) as archive:
if sorted(archive.namelist()) != ['app.txt', 'manifest.json']:
raise ValueError('unexpected member set')
received = json.loads(archive.read('manifest.json'))
if (received['producer_run'] != expected_run
or received['source_sha256'] != source_digest):
raise ValueError('producer identity mismatch')
data = archive.read('app.txt')
if sha256(data).hexdigest() != received['files']['app.txt']:
raise ValueError('payload digest mismatch')
return data
with TemporaryDirectory() as directory:
archive = Path(directory) / 'handoff.zip'
produce(archive, payload)
assert consume(archive, 'local-run-41') == payload
rejected = []
try:
consume(archive, 'local-run-42')
except ValueError as error:
rejected.append(str(error))
produce(archive, payload + b'changed\n')
try:
consume(archive, 'local-run-41')
except ValueError as error:
rejected.append(str(error))
archive.unlink()
try:
consume(archive, 'local-run-41')
except FileNotFoundError:
rejected.append('artifact missing')
assert rejected == ['producer identity mismatch', 'payload digest mismatch', 'artifact missing']
print(json.dumps({'valid_payload_bytes': len(payload), 'rejected': rejected,
'platform_retention_tested': False}))
The valid handoff returns nineteen bytes. Requesting local-run-42 from the archive produced by local-run-41 raises an identity mismatch. Replacing the payload while keeping its old manifest raises a digest mismatch. Deleting the archive produces a missing-artifact failure. The final result lists all three rejected cases; no failure substitutes a different build.
This is a real local archive exercise, not an execution of GitHub’s retention service or a provenance attestation. The source digest is an illustrative identifier, not a verified Git commit. A modified manifest can still tell a consistent lie. The example proves the stated checks and their limits, including that local deletion models absence without proving a platform expiration schedule.
Make the consumer’s gate explicit
Review the actual download action’s failure behavior. GitHub’s documented digest mismatch behavior includes a warning; do not assume that every integrity warning automatically fails a deployment job. If release policy requires rejection, enforce that gate and retain its result alongside the producer identity. Record where durable release bytes are stored, who can retrieve them, and when transient handoff copies may safely expire.
Sources
- GitHub: store and share workflow data, artifact transfer, retention limits, and digest validation, checked 2026-09-13.
- GitHub: artifact attestations, provenance evidence and its verification boundary, checked 2026-09-13.