Chapter 21Lesson 05~230 minutes

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.

CheckpointTwo versionsDigest pinningPermission denialLifecycle 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:

  1. 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.
  2. The latest tag will move to the 2.0.0 digest, but the recorded 1.0.0 digest should remain pullable until its version is deleted.
  3. 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.
  • latest moved 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 consumer has packages:read but a private cross-repository pull is denied. What is the likely missing control?

Why did the denied-write workflow authenticate before its push failed?

What evidence in this checkpoint proves provenance?

Why is deleting an old version a release-governance decision rather than housekeeping?

What chapter follows naturally from governed package distribution?

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.

Next chapter

Dependabot Alerts, Security Updates, Version Updates, Dependency Review, and Policy: Concepts, Architecture, and Mental Model

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.

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