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.
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.
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?
The ID is small control data that binds the downstream job to one exact GitHub artifact record.
Why compute a separate file SHA-256 if upload-artifact already returns an artifact digest?
The artifact digest validates the uploaded artifact payload; the file digest validates the extracted file content expected by the application/lab.
Why is actions: write absent from build and
verify?
Uploading/downloading the same-run artifact does not justify broad API mutation. Only the optional REST deletion job needs Actions write.
What does the missing .hidden-note after download
prove?
That the explicit default hidden-file policy excluded the dot-prefixed file from this upload; it does not prove no other sensitive file was selected.
Why should deletion happen after metadata capture?
Because deleting first destroys the artifact record and can erase evidence needed to explain the original run.
Official references and version notes
- GitHub Docs — Workflow artifacts — artifact purpose, cache distinction and attestation context.
- GitHub Docs — Store and share data with workflow artifacts — upload/download, retention, cross-job transfer and digest validation.
- GitHub REST API — Actions artifacts — artifact ID, size, digest, expiration, download and delete endpoints.
-
actions/upload-artifact v7.0.1
— upload action pinned to
043fb46d1a93c77aae656e7c1c64a875d1fc6a0a. -
actions/download-artifact v8.0.1
— download action pinned to
3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c. - GitHub Docs — Artifact attestations — signed provenance is separate from ordinary artifact storage/digest verification.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.