Checkpoint Lab — GitHub Packages, Container Registry, Package Permissions, Provenance, and Distribution
The checkpoint combines the chapter into one distribution record: publish two synthetic versions, capture source and registry identity, consume exact bytes from a clean job, prove a write denial under least privilege, observe a mutable label move, and retire the disposable package only after writing a support/retention policy.
Learning objectives
- Publish and independently consume two versioned GHCR manifests.
- Predict and verify how the latest label changes while an older digest remains addressable.
- Prove a read-only package token cannot publish and preserve the denial evidence.
- Record package/version/permission identity as an operational handoff.
- Delete only disposable versions through a documented lifecycle policy and understand restore limits.
Checkpoint assumptions. GitHub.com + GitHub Free + public disposable repository/package created in Lesson 2. The package contains only synthetic payload text. The workflows use GITHUB_TOKEN; no PAT or paid cloud/registry service is required. If live publishing is unavailable because Packages is disabled by organization/enterprise policy, use the included state fixture and complete the same identity/permission decisions without changing a production registry.
1. Preflight: prove you are still operating on disposable resources
OWNER=$(gh api user --jq .login)
REPO="$OWNER/github-packages-lab"
IMAGE="ghcr.io/${OWNER,,}/github-packages-lab"
gh repo view "$REPO" --json nameWithOwner,visibility,viewerPermission,defaultBranchRef
gh workflow list -R "$REPO"
echo "package=$IMAGE"
Open the package page and verify it is the Chapter 21 synthetic package, public, linked to the disposable repository, and contains version 1.0.0 from Lesson 2. If any observation differs, stop and resolve identity before publishing or deleting anything.
2. Required predictions before the second release
Write down at least these predictions before dispatching:
- Publishing 2.0.0 will create a new container manifest digest because the payload/metadata contains a different version, even if the source workflow commit SHA is unchanged.
-
The
latesttag will move to the 2.0.0 digest, but the recorded 1.0.0 digest should remain pullable until its version is deleted. - The read-only denial workflow will remain unable to push even though the package is public and linked to the repository.
3. Record version 1 identity before changing latest
From the Lesson 2 publisher run summary, create a small local record:
cat > ch21-release-record.txt <<'EOF'
package=ghcr.io/OWNER/github-packages-lab
v1_tag=1.0.0
v1_digest=sha256:REPLACE_WITH_RECORDED_V1_DIGEST
v1_source_sha=REPLACE_WITH_V1_WORKFLOW_HEAD_SHA
visibility=public
publisher_repo=OWNER/github-packages-lab
EOF
cat ch21-release-record.txt
This is not a secret. It is the minimum evidence required to prove later whether a deployment used the same package bytes.
4. Publish version 2.0.0 using the same governed publisher
gh workflow run publish-package.yml -R "$REPO" -f version=2.0.0
RUN2=$(gh run list -R "$REPO" --workflow publish-package.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN2" -R "$REPO" --exit-status
gh run view "$RUN2" -R "$REPO" --log
Record v2_digest and v2_source_sha. Verify
v2_digest != v1_digest. If they are unexpectedly equal,
inspect the actual payload/build metadata rather than assuming
version labels changed content.
5. Compare mutable labels with immutable identifiers
docker pull "$IMAGE:latest"
LATEST_REF=$(docker image inspect --format '{{index .RepoDigests 0}}' "$IMAGE:latest")
echo "$LATEST_REF"
# Now pull the old immutable content explicitly:
docker pull "$IMAGE@sha256:REPLACE_WITH_V1_DIGEST"
docker image inspect --format '{{index .RepoDigests 0}}' "$IMAGE@sha256:REPLACE_WITH_V1_DIGEST"
Expected: latest resolves to the v2
digest, while the v1 digest still resolves independently. This
proves that “latest changed” and “v1 disappeared” are different
events.
6. Consume version 2 from the separate workflow by digest
gh workflow run consume-package.yml -R "$REPO" -f digest='sha256:REPLACE_WITH_V2_DIGEST' -f expected_version=2.0.0
CONSUME2=$(gh run list -R "$REPO" --workflow consume-package.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$CONSUME2" -R "$REPO" --exit-status
gh run view "$CONSUME2" -R "$REPO" --log
Record the consumer run ID. The checkpoint now has producer run/source SHA, package digest, and independent consumer evidence for both versions.
7. Re-run the permission denial after v2 exists
gh workflow run package-denial.yml -R "$REPO"
DENY=$(gh run list -R "$REPO" --workflow package-denial.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$DENY" -R "$REPO"
gh run view "$DENY" -R "$REPO" --log
The controlled job should report that the Docker push failed as expected while the job conclusion is success. This is a security test: it demonstrates that package write permission did not creep into a read-only consumer path.
8. Cross-repository denial fixture: explain the missing boundary
Do not create a private package merely to force a billable/restricted scenario. Instead, evaluate this realistic fixture:
Producer repository: acme/platform-image
Package: ghcr.io/acme/platform-image (private)
Consumer repository: acme/service-a
Consumer job: permissions: {contents: read, packages: read}
Package Manage Actions access: service-a NOT listed
Observed pull: denied / 403
Diagnosis: the job permission is necessary but
package-side repository authorization is missing. Correct by adding
acme/service-a with Read, not by
giving the workflow a publisher token. If
service-a were public, first evaluate GitHub’s fork
warning for private-package access.
9. Write the provenance statement without overclaiming
Your checkpoint produced source SHA and image digest, but it did not generate an artifact attestation. Write that accurately:
Provenance status for Chapter 21 lab:
- registry identity recorded: YES (sha256 digest)
- source workflow/run identity recorded: YES
- signed artifact attestation generated: NO
- attestation verified by consumer: NO
- production claim permitted: "digest-pinned synthetic package", not "verified provenance"
This prepares the evidence boundary for Chapter 25 instead of pretending every GitHub package is attested.
10. Produce a package distribution and retention policy
| Control | Checkpoint policy | Production rationale |
|---|---|---|
| Naming | Account/org namespace + stable package name | Avoid registry/namespace ambiguity |
| Publish identity |
GITHUB_TOKEN, publisher job only,
packages: write
|
Short-lived least privilege |
| Consumer identity |
Public anonymous pull or approved repo +
packages: read
|
No shared writer credential |
| Release identity | Semantic version/tag + recorded digest | Human navigation plus immutable machine identity |
| Mutable aliases |
latest never accepted as release evidence
|
Alias may move |
| Provenance | Attestation required before claiming verified build origin | Hosting/source link alone is insufficient |
| Retention | Keep every supported/rollback digest; delete only after EOL/dependency review | Cleanup must not break supported releases |
| Visibility | Review before public; public cannot revert to private | Prevent irreversible disclosure |
| Deletion | Admin-only, change record, restore window understood | Destructive and consumer-impacting |
11. Cleanup: delete only the disposable package after preserving evidence
Destructive cleanup. Deleting package versions or the whole package can break consumers. Confirm the package name and that no real project depends on it. Do not reuse these steps on production packages.
From your profile → Packages →
github-packages-lab, review all versions and the
tag/digest mapping one final time. Delete the v1/v2 disposable
versions (or the entire disposable package) using the current
Package settings controls. GitHub requires admin access. Public
versions with more than 5,000 downloads cannot be deleted normally;
that should never apply to this fresh lab.
Record the deletion timestamp. GitHub normally permits restore within 30 days if the namespace/version has not been reused. Do not immediately publish a different package into the same namespace if you want rollback of the cleanup to remain possible.
Archive the disposable repository instead of permanently deleting it if you want the workflow/run evidence to remain visible:
gh repo archive "$REPO" --yes
12. Final verification checklist
- Two publisher runs recorded source SHA and distinct immutable digests.
- The separate consumer workflow verified v1/v2 by digest.
-
latestmoved to v2 without changing the v1 digest identity. - The read-only workflow could pull but not push.
- Package visibility/linkage/access model was recorded.
- No PAT, registry password, Docker auth config, or token was uploaded/logged.
- Provenance was described accurately as not attested in this chapter.
- Deletion occurred only after the retention/support policy and evidence record were complete.
Knowledge check
After publishing v2, latest points to v2 but production must roll back to v1. What should deployment use?
The recorded v1 digest, e.g. ghcr.io/owner/package@sha256:..., not the mutable latest tag.
The consumer has packages:read but a private cross-repository pull is denied. What is the likely missing control?
Package-side Actions access for that consumer repository (or the intended inherited permission relationship).
Why did the denied-write workflow authenticate before its push failed?
Authentication established GITHUB_TOKEN identity; packages:read intentionally withheld publish authorization.
What evidence in this checkpoint proves provenance?
None cryptographically. The lab records source and digest correlation but deliberately generates no attestation, so it must not claim verified provenance.
Why is deleting an old version a release-governance decision rather than housekeeping?
Consumers, rollback procedures, and supported releases may still depend on that immutable version/digest. Deletion can destroy reproducibility.
What chapter follows naturally from governed package distribution?
Chapter 22: dependency graph and Dependabot policy, where consumers must continuously understand and update the dependencies/packages they rely on.
Production operating model and bridge to Chapter 22
Chapter 21 adds a distribution control plane to the production GitHub model: packages have namespaces, independent permission models, mutable human labels, immutable content identifiers, lifecycle rules, and optional provenance evidence. A production handoff should be able to answer who may publish, who may consume, which exact digest is supported, how provenance is verified, and when deletion is allowed.
Chapter 22 moves from publishing dependencies to managing dependency risk: dependency graphs, Dependabot alerts, update policies, private registry access, and review gates.
Official references
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.