Package Registry, Dependency Proxy, Package Formats, Permissions, and Artifact Distribution: Guided Hands-On Workflow and Core Operations
Publish and consume a tiny deterministic Generic Package with CI_JOB_TOKEN, prove exact version and SHA-256 identity, inspect package metadata, and clean up safely with a no-runner fallback.
Learning objectives
- Inspect registry/project state before publishing anything.
-
Publish and consume one deterministic Generic Package using only
CI_JOB_TOKEN. - Verify package bytes and registry SHA-256/provenance independently.
- Inspect current package metadata with glab/API without exposing credentials.
- Delete only the exact disposable version after a consumer-impact check.
1. Workflow goal: prove one complete package lifecycle without creating a long-lived credential
The mandatory exercise uses a disposable GitLab project and one tiny
Generic Package. A producer job publishes
payload.txt with its automatically supplied
CI_JOB_TOKEN. A consumer job downloads the exact
package name/version/file and verifies independently reconstructed
content. Then you inspect metadata with your existing
glab session and delete the lab package only after
recording provenance.
2. Preflight: scope, roles, runner path, and zero-secret rule
- Offering/tier: GitLab.com, Self-Managed, or Dedicated; Free is sufficient.
-
Project: dedicated disposable project such as
ch22-package-lab. - Role: Developer can normally publish; use Maintainer/Owner on your own disposable project for cleanup.
- Runner: one tiny ordinary runner job. If hosted compute is unavailable, use the static/no-runner fixture path below.
-
Credential: no token is created. The job uses
CI_JOB_TOKEN; never print it or enable debug tracing. - Side effect: package publication and deletion are intentional registry mutations. Do not run against a production package project.
3. Inspect package state before changing it
Record project and source state:
glab repo view --output json | jq '{path_with_namespace:.path_with_namespace,id:.id,default_branch:.default_branch}'
git status --short --branch
git rev-parse HEAD
PROJECT_ID="<disposable-project-id>"
glab api "projects/$PROJECT_ID/packages?package_type=generic&per_page=100" \
| jq 'map({id,name,version,status,created_at})'
Expected: either an empty list or only synthetic lab packages you recognize. If you see valuable package versions, create a fresh project rather than “cleaning” the existing one.
4. Predict the package URL before publication
Use these deterministic coordinates:
| Coordinate | Value |
|---|---|
| Project | Current disposable project ID |
| Package name | ch22-synthetic |
| Version | 0.0.<CI_PIPELINE_IID> |
| File | payload.txt |
| Content |
chapter22|sha=<CI_COMMIT_SHA>|pipeline=<CI_PIPELINE_ID>
|
Prediction 1: after the publish job, exactly this project gains a Generic Package record. Prediction 2: the consumer job needs no persistent secret because its own job token authenticates to the same registry.
5. Create the smallest useful publish→consume pipeline
The example installs curl in a small Alpine container.
In production, pin an approved image by immutable digest; the
chapter keeps the image detail secondary to the registry contract.
stages: [publish, verify]
publish_package:
image: alpine:3.22
stage: publish
before_script:
- apk add --no-cache curl
script:
- VERSION="0.0.${CI_PIPELINE_IID}"
- printf 'chapter22|sha=%s|pipeline=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > payload.txt
- sha256sum payload.txt
- |
curl --fail-with-body --location \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--upload-file payload.txt \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/ch22-synthetic/${VERSION}/payload.txt"
- echo "published_version=${VERSION}"
consume_exact_package:
image: alpine:3.22
stage: verify
needs: [publish_package]
before_script:
- apk add --no-cache curl
script:
- VERSION="0.0.${CI_PIPELINE_IID}"
- |
curl --fail-with-body --location \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output downloaded.txt \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/ch22-synthetic/${VERSION}/payload.txt"
- printf 'chapter22|sha=%s|pipeline=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > expected.txt
- sha256sum expected.txt downloaded.txt
- cmp expected.txt downloaded.txt
The consumer deliberately does not download a job
artifact. Its dependency is the Package Registry coordinate.
needs only enforces scheduling here.
6. Validate before spending compute or writing package state
Use the Pipeline Editor/CI Lint or current API/UI validation. The key checks are YAML validity, correct stage names, and that no rule suppresses either job. If you have no runner quota, stop after validation and use the no-runner fixture in section 11.
7. Run and inspect the pipeline without exposing the credential
Commit on a disposable branch, push, then inspect:
git switch -c ch22/package-lab
git add .gitlab-ci.yml
git commit -m "lab: publish deterministic generic package"
git push -u origin ch22/package-lab
glab ci status --wait
glab ci get --output json | jq '{id,status,ref,sha}'
Expected producer evidence: a SHA-256 line, HTTP success, and a
non-secret published_version. Expected consumer
evidence: two equal SHA-256 values and successful cmp.
The token value must never appear.
8. Inspect package metadata and provenance outside the job
Take the package version from the job log or pipeline IID, then
query packages using your already-authenticated
glab session:
PROJECT_ID="<disposable-project-id>"
VERSION="0.0.<pipeline-iid>"
glab api "projects/$PROJECT_ID/packages?package_type=generic&package_name=ch22-synthetic&package_version=$VERSION&per_page=100" \
| jq 'map({id,name,version,package_type,status,created_at,pipeline})'
Record the returned package ID and producer pipeline/SHA. If a
package is still in a transient processing state, do
not treat incomplete metadata as authoritative; re-check after
processing completes.
9. Inspect the registry-stored file checksum
With the package ID:
PACKAGE_ID="<package-id>"
glab api "projects/$PROJECT_ID/packages/$PACKAGE_ID/package_files" \
| jq 'map({id,file_name,size,file_sha256,created_at})'
Compare file_sha256 to the SHA-256 printed by the
deterministic producer/consumer. This connects registry metadata,
downloaded bytes, and source pipeline identity.
10. Optional ergonomic path: current
glab packages commands
Current glab packages upload and
glab packages download work with Generic Packages.
glab packages download verifies the stored checksum by
default unless explicitly told not to. Use these commands when a
human-authenticated workflow is appropriate; do not replace
short-lived CI job-token automation with a broad personal token
merely for convenience.
glab packages download \
--name ch22-synthetic \
--version "$VERSION" \
--filename payload.txt \
--path ./verified-payload.txt
11. No-runner / no-quota fallback
You can still learn the contract without a remote registry write. Locally generate the exact payload and identity record:
SHA="$(git rev-parse HEAD)"
PIPELINE_ID="4242"
PIPELINE_IID="42"
printf 'chapter22|sha=%s|pipeline=%s\n' "$SHA" "$PIPELINE_ID" > payload.txt
sha256sum payload.txt
printf 'projects/<id>/packages/generic/ch22-synthetic/0.0.%s/payload.txt\n' "$PIPELINE_IID"
Then review the expected 201 Created publish response
and package metadata fixture from the lesson. This path changes no
GitLab package state.
12. Optional Dependency Proxy inspection
Container image proxy (Free): if your disposable
project belongs to a disposable group and the proxy is enabled,
inspect Operate → Dependency Proxy and the predefined
prefix. You may pull a tiny Docker Hub image through it if quota
allows. Record the immutable digest rather than trusting only a tag.
Package proxy (Premium/Ultimate, Beta): use documentation/fixture inspection only in the mandatory course. It uses Package Registry-compatible authentication and currently adds a separate upstream package-cache configuration surface.
13. Delete only after consumer impact is documented
Package deletion is destructive. Confirm that the package ID/version belongs to this disposable run, record checksum/provenance, then delete with your authenticated Maintainer/Owner session:
test -n "$PACKAGE_ID"
printf 'About to delete package id=%s version=%s from disposable project %s\n' \
"$PACKAGE_ID" "$VERSION" "$PROJECT_ID"
# Only after the identity check above:
glab api -X DELETE "projects/$PROJECT_ID/packages/$PACKAGE_ID"
# Verify absence:
glab api "projects/$PROJECT_ID/packages?package_type=generic&package_name=ch22-synthetic&package_version=$VERSION&per_page=100" \
| jq 'length'
Expected final count: 0 for that exact version. In
ecosystems with request forwarding, deleting a private package while
forwarding remains enabled can create dependency-confusion risk;
evaluate that before production cleanup.
14. Challenge: choose the correct distribution surface
You have three outputs: a 20-minute test report used only by the next job, a versioned CLI binary used by other repositories for six months, and an OCI application image. Choose job artifact, Package Registry, or Container Registry for each and justify identity, retention, and consumer protocol. If you choose “Package Registry for everything,” revisit the object boundaries from Lesson 1.
Knowledge check
Why does the consumer use a Package Registry GET instead of a job artifact download?
The exercise proves the package is independently resolvable by project/name/version/file, not merely passed between jobs in one pipeline.
What proves the package bytes were not silently changed?
The consumer reconstructs expected deterministic content and compares it byte-for-byte; the Packages API also records the Generic Package file SHA-256.
Why is a persistent deploy token unnecessary in the mandatory workflow?
Both producer and consumer run in GitLab CI, where the documented short-lived CI_JOB_TOKEN already authenticates to the Package Registry.
What should you do if package metadata still reports processing?
Wait and re-inspect rather than making promotion/deletion decisions from incomplete processing-state metadata.
What must happen before deleting the synthetic package?
Confirm its exact package ID/version, record checksum/provenance, and prove there are no real downstream consumers or rollback dependencies.
Summary
You completed the full Free-compatible package lifecycle using a short-lived CI identity: deterministic publish, exact-version consume, independent content reconstruction, registry metadata/checksum inspection, and guarded deletion. No persistent token or paid proxy feature was required.
Official references
Primary sources used for the current GitLab 19.3 behavior taught in this lesson:
- GitLab Docs — Package Registry
- GitLab Docs — Supported package managers and functionality
- GitLab Docs — Generic Packages
- GitLab Docs — Packages API
- GitLab Docs — CI/CD job token
- GitLab Docs — Dependency Proxy for container images
- GitLab Docs — Dependency Proxy for packages
- GitLab Docs — Reduce Package Registry storage
- GitLab CLI — glab packages upload
- GitLab CLI — glab packages download
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.