GitHub Actions can upload and download build artifacts, but they’re not a general-purpose file storage solution.

Here’s how it works in practice. Imagine a workflow that first builds a Docker image and then uploads it as an artifact, followed by a separate job that downloads and uses that image.

name: Build and Deploy Docker Image

on: [push]

jobs:
  build_and_push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build Docker image
        id: docker_build
        run: |
          docker build -t my-docker-image:latest .

          echo "::set-output name=image_tag::$(docker images --format '{{.ID}}' my-docker-image:latest)"


      - name: Upload Docker image as artifact
        uses: actions/upload-artifact@v4
        with:
          name: docker-image
          path: /var/lib/docker/image/my-docker-image:latest # This path is conceptual, actual artifact handling differs

  deploy:
    runs-on: ubuntu-latest
    needs: build_and_push
    steps:
      - name: Download Docker image artifact
        uses: actions/download-artifact@v4
        with:
          name: docker-image
          path: ./downloaded_artifacts

      - name: Load Docker image from artifact
        run: |
          # This is a conceptual representation. Actual Docker image loading from artifacts
          # would involve saving the image to a tarball and then loading it.
          # For example:
          # docker save my-docker-image:latest | gzip > my-docker-image.tar.gz
          # docker load < my-docker-image.tar.gz
          echo "Docker image artifact downloaded to ./downloaded_artifacts"
          ls -l ./downloaded_artifacts

This example illustrates the core concept: one job produces something, and another job consumes it. The upload-artifact action takes files or directories and bundles them, while download-artifact unpacks them in a subsequent job.

The problem this solves is state management across jobs and workflows. Without artifacts, each job starts from a clean slate. If job A produces a compiled binary, and job B needs that binary to run tests or deploy, job B wouldn’t have access to it unless it was explicitly passed via artifacts. This is crucial for complex build pipelines where intermediate outputs are essential for later stages.

Internally, actions/upload-artifact bundles the specified files into a compressed archive (like a .zip or .tar.gz). This archive is then uploaded to GitHub’s internal artifact storage, associated with the specific workflow run. When actions/download-artifact is used in a dependent job, it fetches this archive from storage and extracts its contents to the specified path. The name parameter in both actions is the key that links the uploaded artifact to the downloaded one.

The levers you control are primarily the name and path parameters. name acts as the identifier for the artifact. It’s how the downloading job knows which artifact to fetch. path in upload-artifact specifies what to upload (files, directories, or globs), and path in download-artifact specifies where to put the downloaded contents. You can also control retention policies for artifacts at the repository level, determining how long they are kept.

A detail that often trips people up is that artifacts are tied to a specific workflow run. If you trigger the same workflow twice concurrently, each run will have its own independent set of artifacts. You cannot directly access artifacts from a different workflow run using the standard download-artifact action. If you need to share artifacts between different workflow runs, you’d typically need to push them to external storage like an S3 bucket or a package registry.

The next concept you’ll likely encounter is managing large artifacts or when you need finer-grained control over artifact storage and retrieval.

Want structured learning?

Take the full Github-actions course →