Skip to content
MINH VO A working notebook
by an engineer in Vietnam
Foundation9 min read

Artifacts and dependency caches have different jobs

Use workflow artifacts to transfer tested outputs and dependency caches to avoid repeated downloads, with separate identity and retention rules.

Miniature conveyor line with three stations and a curved side track

A workflow artifact carries an output that another job or person needs to inspect or use. A dependency cache helps a job avoid repeating work that can be reconstructed from declared inputs. Treating them as interchangeable can make a release depend on whichever cached files happened to be available.

For a developer connecting build and deployment jobs, the useful distinction is whether the next job requires this exact output or can reconstruct an input. This article assumes basic YAML and shell familiarity. Keep dependency downloads in a disposable cache; record the tested release output separately. The examples use harmless text files and local Python byte strings.

Follow one output from producer to consumer

This workflow fragment builds a harmless text artifact and its checksum. A second job downloads the artifact and checks those bytes:

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - run: |
          printf '%s\n' 'example release bytes' > app.txt
          sha256sum app.txt > app.sha256
      - uses: actions/upload-artifact@v4
        with:
          name: example-release
          path: |
            app.txt
            app.sha256
  inspect:
    needs: build
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: example-release
      - run: sha256sum -c app.sha256

The action references use release-family tags for readability. A maintained release workflow should use reviewed immutable action references according to its supply-chain policy. The example transfers a fixture, not a deployable application or a signed release.

The dependency between jobs orders the consumer after the producer. It does not share the producer’s disk with the consumer. Upload and download provide the explicit transfer channel.

A dependency cache supplies reusable inputs, while a workflow artifact transfers explicit output bytes to a consumer that verifies them.View full-size image ↗

The application output moves through the artifact store. A package-download cache feeds the build separately and is not the identity of the release being promoted.

A checksum verifies a particular relationship

The consumer’s checksum command tests whether the downloaded file matches the recorded checksum. If an attacker can replace both the file and the checksum, that comparison alone does not establish publisher authenticity. A trusted workflow identity, provenance or signature policy can supply additional evidence about who produced the bytes.

Keep the checksum’s scope clear. It can detect accidental mismatch between a known expected value and an artifact, but it does not certify the application’s behavior. Run application tests against the same output that will be promoted when that distinction matters.

The workflow-artifact documentation describes storing and sharing workflow outputs. Artifact identifiers, workflow runs and retention are part of that contract. A name that is reused across runs is not enough by itself to identify which release a downstream process should trust.

A cache should be safely disposable

A dependency cache can hold package archives or tool downloads that the package manager would otherwise retrieve again. On a cache miss, installation should still produce the intended result from the lockfile and authenticated package sources.

If a build works only when an old cache exists, it has an undeclared dependency. A clean-builder check is useful because it reveals those hidden inputs. The correct repair may be adding a missing generated file to the build process or correcting a dependency declaration, rather than preserving the cache forever.

Cache keys describe when reuse is likely to be compatible. Operating system, architecture, toolchain and dependency-lock content can all matter. A broad fallback key can reuse partial downloads efficiently, but the package manager still needs to resolve and verify the requested dependencies.

A release binary selected through a broad cache key has a different risk. Restoring whatever binary matches a broad key can bypass the build or test that the release process intended to require. Use an explicit artifact reference when the output itself is the object being reviewed.

Retention and recovery differ

Caches can be evicted according to platform limits and access patterns. Workflow artifacts also have retention and deletion rules. Neither should be assumed to remain available indefinitely merely because a run once succeeded.

If an artifact is needed for production rollback or audit beyond ordinary workflow retention, promote it to a registry or release store with the required lifecycle. Preserve its digest, provenance and compatibility information there. A link to an expired workflow artifact is not a recovery plan.

Store only the outputs needed by the consumer. Uploading an entire workspace can include source-control metadata, credentials, caches or temporary test data. A narrow artifact path list makes both retention cost and data exposure easier to review.

Name the object the next job is allowed to consume

A consumer should bind its input to the expected workflow run, source revision and artifact identity. Triggering another workflow or accepting an artifact name supplied by an untrusted event adds an authorization question: which producer is allowed to supply the release?

The toy checksum example does not model a cross-workflow producer-authentication boundary: both jobs are declared together and only transfer harmless fixture content. That choice does not establish that every job in a real workflow is trusted. A privileged deployment consuming outputs from another workflow needs to validate the producer’s context and permissions before using the bytes.

The practical distinction is visible in a failure test. Remove the cache and the build may need additional download or reconstruction work while producing the same intended output. This is a correctness requirement, not a guarantee about elapsed time. Remove the required artifact and the consumer should fail clearly because its declared release input is unavailable. Those two outcomes show that optimization state and release evidence have separate roles.

Exercise the two failure paths locally

A small model makes the difference observable without a CI account. The declared dependency is a byte string, and its expected digest stands in for an independently selected dependency identity. A source function counts how often it is called. It does not contact a package registry, compile code or measure network latency.

from hashlib import sha256

canonical_dependency = b"dependency-v1\n"
expected_dependency = sha256(canonical_dependency).hexdigest()
source_calls = 0

def source():
    global source_calls
    source_calls += 1
    return canonical_dependency

def build(cached):
    dependency = source() if cached is None else cached
    if sha256(dependency).hexdigest() != expected_dependency:
        raise ValueError("dependency mismatch")
    return b"release:" + dependency

def consume(artifact, expected):
    if artifact is None:
        raise FileNotFoundError("required artifact missing")
    if sha256(artifact).hexdigest() != expected:
        raise ValueError("artifact mismatch")
    return artifact

cold_output = build(None)
print("cold source calls:", source_calls)
source_calls = 0
warm_output = build(canonical_dependency)
print("warm source calls:", source_calls)
print("same output:", cold_output == warm_output)
release_digest = sha256(cold_output).hexdigest()
print("consumer accepted:", consume(warm_output, release_digest) == cold_output)

for label, operation in [
    ("bad cache", lambda: build(b"different dependency\n")),
    ("missing artifact", lambda: consume(None, release_digest)),
    ("changed artifact", lambda: consume(b"different release\n", release_digest)),
]:
    try:
        operation()
    except (ValueError, FileNotFoundError) as error:
        print(label + ":", type(error).__name__, str(error))

The cold path makes one source call; the warm path makes none. Both produce the same bytes, and the consumer accepts them against the same release digest. The three final lines report a dependency mismatch, a missing required artifact and an artifact mismatch, respectively. These observations concern the declared local model. They do not prove that two real compiler invocations are reproducible or that a particular cache will save time.

The corrupt cache deliberately fails instead of silently falling back. A real dependency tool might discard a bad archive and retry an authenticated source. That is a different recovery policy, and it needs its own visible verification step. Quietly accepting corrupt bytes to preserve a cache hit would violate the input contract. Conversely, a cache miss is ordinary absence and takes the explicitly defined reconstruction path.

A local fault table contrasts a valid cache hit, a cache miss, a corrupt cache and a missing required artifact.View full-size image ↗

The first two rows converge on identical release bytes. The last two stop at different boundaries: dependency verification before output construction, and artifact availability before consumption. A failure message should identify that boundary rather than telling every caller to rerun the whole workflow.

Decide what a restored directory proves

The dependency-caching reference distinguishes an exact key match from fallback matching. A restored directory can contain useful old downloads without representing a complete match for the current dependency set. Let the dependency resolver check the current lockfile and available objects. Do not infer that installation or verification can always be skipped merely because some directory was restored.

The platform does not let you edit the contents of an existing cache entry in place; a new key creates another entry. This does not turn a broad selection rule into a release identity. Compatibility is a property of the selected bytes and declared inputs, not just the fact that a service returned an entry.

File extension is not the deciding factor. A compiler’s intermediate output can be a legitimate cache entry if the build system has a correct input identity and can reconstruct it when absent. The very same binary format can be an artifact when a release reviewer needs those exact tested bytes. Ask what promise the consumer relies on, then choose storage, lookup and verification rules that preserve it.

For a final exercise, change only the artifact bytes after the shell producer runs and retain the old checksum. The consumer must fail. Then regenerate the checksum for the changed file; the shell comparison now succeeds, showing why a co-transferred checksum is not independent authorization. In the Python model, changing both an artifact and the caller’s expected digest similarly changes what is accepted. Preserving an expected digest only matters when its selection is trustworthy.

Next, make the source function return the wrong dependency. A cold build should fail the same digest check as a corrupt warm cache. If you choose a discard-and-retry cache policy, keep that source verification. Finally, remove the required artifact while leaving a dependency cache available: a consumer tasked with promoting an already tested output should report its missing input, rather than inventing a substitute release from whatever files it can find.

Sources & further reading

  1. GitHub workflow artifacts
  2. GitHub dependency caching reference
← Back to the journal
All notes

Illustration

100%