Chapter 22Lesson 02~330 minutes

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.

Hands-onCI_JOB_TOKENGeneric PackagesSHA-256APICleanup

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.
Availability baseline (verified 2026-08-22 against current GitLab 19.3 documentation). GitLab Package Registry, Generic Packages, CI/CD job-token authentication to the registry, the Packages API, and the container-image Dependency Proxy are available on Free/Premium/Ultimate across GitLab.com, Self-Managed, and Dedicated. The container-image Dependency Proxy is group-scoped, can be disabled by administrators, and currently proxies Docker Hub container images. The newer dependency proxy for packages is Premium/Ultimate and Beta, so it is optional/read-only here. The mandatory labs use one tiny Generic Package in a disposable project and do not require a paid tier, cloud account, private upstream registry, persistent token, or privileged runner.

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?

What proves the package bytes were not silently changed?

Why is a persistent deploy token unnecessary in the mandatory workflow?

What should you do if package metadata still reports processing?

What must happen before deleting the synthetic package?

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:

Next lesson

Configuration, Design Choices, and Tradeoffs

Turn the mechanics into architecture policy: choose registry boundaries, identity type, immutability rules, and proxy strategy without over-centralizing or widening credential blast radius.

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.