A dependency between jobs is not a dependency between artifacts

needs controls ordering and shares outputs, and each job still starts on a fresh machine with an empty workspace.

jobs:
  build:
    outputs:
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: docker/build-push-action@v3
        id: push

  test:
    needs: build
    steps:
      - uses: actions/checkout@v3    # ← again. nothing carried over.
      - run: docker pull "registry/app@${{ needs.build.outputs.digest }}"

Outputs are strings and are limited in size, so anything larger has to go through an artifact or a registry — which is the reason to push the image in the build job rather than trying to hand it over. The re-checkout in every job is the cost of the model and is also what makes jobs independently retryable.