Chapter 13Lesson 02~205 minutes

Artifacts, Retention, Cross-Job Data, and Workflow Result Management: Guided Hands-On Workflow

This lesson builds a disposable two-job artifact pipeline. You will create a tiny file, upload it with explicit retention and hidden-file policy, pass artifact identity through job outputs, download it by ID, verify both the artifact service digest and the file digest, inspect metadata, and optionally delete only the lab artifact.

Cross-job transferGITHUB_OUTPUTHidden filesREST metadataSafe deletion

Learning objectives

  • Build a tiny deterministic file and compute a separate file SHA-256 before upload.
  • Upload with explicit name, path, retention, hidden-file and compression behavior using a pinned action.
  • Pass artifact ID/digest and small control values through job outputs instead of ambient environment state.
  • Download by artifact ID, validate content and inspect REST metadata including size/digest/expiry.
  • Perform exact-ID cleanup only after evidence is preserved, using a narrowly scoped Actions write permission.

1. Disposable repository and preflight

Create a throwaway GitHub.com repository named gha-artifact-lab. The mandatory workflow uses no checkout, cloud account, deployment target or long-lived secret. It creates only one tiny text artifact in the current repository. Use ubuntu-24.04.

  • Confirm Actions is enabled for the disposable repository.
  • Keep the top-level token policy at permissions: {}.
  • Record the workflow file SHA and the resulting run ID/attempt.
  • Use only fake/synthetic content. Never place real credentials in the test directory.
  • Choose whether the optional cleanup job should delete the exact artifact after verification.

2. Build → upload → transfer identity → download → verify

The producer job creates a deterministic file and computes its file SHA-256. It then uploads that file and exposes the artifact ID, artifact digest and file digest as job outputs. The consumer uses the artifact ID—not a guessed name—to select the exact record.

name: artifact-lab
on:
  workflow_dispatch:
    inputs:
      delete_after:
        description: Delete the lab artifact after verification
        required: true
        type: boolean
        default: false
permissions: {}

jobs:
  build:
    runs-on: ubuntu-24.04
    outputs:
      artifact_id: ${{ steps.upload.outputs.artifact-id }}
      artifact_digest: ${{ steps.upload.outputs.artifact-digest }}
      artifact_url: ${{ steps.upload.outputs.artifact-url }}
      file_sha256: ${{ steps.filehash.outputs.sha256 }}
      label: ${{ steps.meta.outputs.label }}
    steps:
      - name: Create deterministic build file
        shell: bash
        run: |
          mkdir -p dist
          printf 'source_sha=%s
run_id=%s
'             "$GITHUB_SHA" "$GITHUB_RUN_ID" > dist/build.txt
          printf 'harmless hidden note
' > dist/.hidden-note
          find dist -maxdepth 1 -type f -printf '%f
' | sort

      - id: filehash
        name: Hash the file content
        shell: bash
        run: |
          sha="$(sha256sum dist/build.txt | awk '{print $1}')"
          echo "sha256=$sha" >> "$GITHUB_OUTPUT"

      - id: meta
        shell: bash
        run: echo 'label=chapter13-lab' >> "$GITHUB_OUTPUT"

      - id: upload
        name: Upload selected build output
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: build-${{ github.run_id }}
          path: dist/
          if-no-files-found: error
          retention-days: 1
          compression-level: 6
          include-hidden-files: false

  verify:
    needs: build
    runs-on: ubuntu-24.04
    permissions:
      actions: read
    env:
      GH_TOKEN: ${{ github.token }}
      ARTIFACT_ID: ${{ needs.build.outputs.artifact_id }}
      EXPECTED_FILE_SHA: ${{ needs.build.outputs.file_sha256 }}
    steps:
      - name: Download exact artifact record
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          artifact-ids: ${{ needs.build.outputs.artifact_id }}
          path: ${{ runner.temp }}/chapter13-download
          digest-mismatch: error

      - name: Verify content and hidden-file boundary
        shell: bash
        run: |
          root="$RUNNER_TEMP/chapter13-download"
          test -f "$root/build.txt"
          test ! -e "$root/.hidden-note"
          actual="$(sha256sum "$root/build.txt" | awk '{print $1}')"
          test "$actual" = "$EXPECTED_FILE_SHA"
          cat "$root/build.txt"

      - name: Inspect artifact metadata read-only
        shell: bash
        run: |
          gh api             -H 'X-GitHub-Api-Version: 2026-03-10'             "repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID"             --jq '{id,name,size_in_bytes,digest,expired,created_at,expires_at,workflow_run}'

  cleanup:
    if: ${{ inputs.delete_after && needs.verify.result == 'success' }}
    needs: [build, verify]
    runs-on: ubuntu-24.04
    permissions:
      actions: write
    env:
      GH_TOKEN: ${{ github.token }}
      ARTIFACT_ID: ${{ needs.build.outputs.artifact_id }}
    steps:
      - name: Delete only the verified lab artifact
        shell: bash
        run: |
          test -n "$ARTIFACT_ID"
          gh api --method DELETE             -H 'X-GitHub-Api-Version: 2026-03-10'             "repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID"

3. Why each layer exists

Mechanism State it changes/reads Reason
dist/build.txt runner filesystem large/file data must exist as a file before upload
file_sha256 job output workflow control data small scalar used to verify extracted file content
upload action outputs GitHub artifact identity artifact ID/digest/URL are assigned only after upload succeeds
download by artifact-ids consumer selection binds consumer to the exact record instead of a guessed human name
digest-mismatch: error download integrity policy fail the consumer if artifact integrity validation fails
REST metadata GET GitHub-owned artifact state proves size, service digest, expiry and producing workflow run
cleanup actions: write artifact lifecycle write permission exists only on exact-ID cleanup job

4. Compare the job output with the artifact

The label and file_sha256 are small control values, so job outputs are appropriate. build.txt is file content and must cross a runner boundary, so an artifact is appropriate. Do not base64-encode a large file merely to squeeze it into an output.

5. Prove retention instead of assuming it

The upload requests one-day retention. The actual expiry is a GitHub-owned record and should be inspected using expires_at. If repository policy is stricter than your requested value, policy wins. Changing repository retention settings later does not retroactively rewrite existing artifacts.

6. Deletion is cleanup, not diagnosis

Run once with delete_after=false and save the metadata/evidence. Only then test delete_after=true. The cleanup job uses actions: write solely because deletion mutates GitHub artifact state; the build and verify jobs remain unprivileged/read-only.

Exact-ID guard

Never implement cleanup as “delete every artifact with this prefix” while diagnosing. Preserve the original record first and delete only the verified disposable artifact ID.

7. Mini challenge: choose the transport

You need to pass a 20-character semantic version to the next job, retain a 25 MB test report for a week, and speed up repeated dependency installation. Choose job output, artifact and cache respectively, and state the evidence that proves each mechanism did what you intended.

Knowledge check

Why pass artifact-id through a job output?

Why compute a separate file SHA-256 if upload-artifact already returns an artifact digest?

Why is actions: write absent from build and verify?

What does the missing .hidden-note after download prove?

Why should deletion happen after metadata capture?

Next lesson

Choose artifact policy rather than defaults

Lesson 3 turns the lab into design decisions about caches, outputs, names/IDs, overwrite, retention, release evidence and signed provenance.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked on 2026-09-09. Mandatory examples target GitHub.com and ubuntu-24.04. actions/upload-artifact v7.0.1 and actions/download-artifact v8.0.1 are pinned by full commit SHA. Upload v7 excludes dot-prefixed hidden files by default, supports compression levels 0–9, returns artifact ID/URL/SHA-256 digest, and treats overwrite as delete-and-create rather than in-place mutation. Download v8 can select by artifact ID and defaults digest mismatch handling to error. Artifact/log retention is policy bounded: GitHub documents 1–90 days for public repositories and up to 400 days for private repositories when repository/organization/enterprise policy permits. Upload-artifact v4+ is not supported on GitHub Enterprise Server; GHES users must use an appliance-compatible artifact action/backend and must not copy GitHub.com majors blindly.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.