Chapter 03Lesson 05~190 minutes

Checkpoint Lab — Projects, Repository Settings, Visibility, Forks, Protected Resources, and Project Templates

Build a disposable GitLab project from a known local source state, predict every important hosted-versus-Git transition, prove the result through UI, Git, glab, and API evidence, and clean up safely.

CheckpointState proofDefault branchDecision tableCleanupProduction model

Learning objectives

  • Create a disposable GitLab project from a known local Git source state without creating competing server history.
  • Predict default-branch, remote, visibility, namespace, and protected-ref observations before the first push and verify each prediction independently.
  • Compare blank, fork, and template-derived designs using an explicit governance decision table.
  • Diagnose one intentionally broken project/remote condition without escalating privilege or using destructive recovery.
  • Produce a compact evidence packet and safe cleanup/rollback plan that bridges project governance into Chapter 04 access governance.
Availability baseline (verified 2026-08-21). Project creation, blank projects, forks, private/public visibility, basic protected branches/tags, archiving, and the Projects API are available across Free, Premium, and Ultimate on GitLab.com, Self-Managed, and Dedicated. Internal visibility is not available for new GitLab.com projects; it remains available on Self-Managed and Dedicated. Group custom project templates are Premium/Ultimate. Project topics are currently documented as Beta. Exact UI labels and administrator restrictions can vary by GitLab version and instance policy.

1. Checkpoint mission and safety contract

You are onboarding a small synthetic service called catalog-api. A local Git repository already contains the authoritative first commit. Your job is to create an empty Private GitLab project, connect it without generating an unrelated server commit, prove the first push and default-branch state, inspect hosted metadata/protection, and document why this design is safer than the obvious alternatives.

  • Required: Git, glab, a GitLab Free account, and a disposable personal namespace.
  • No runner, pipeline minutes, cloud account, registry push, token creation, or paid feature is required.
  • Do not use production source or customer data.
  • Do not change project visibility to Public as part of the lab.
  • Deletion is optional and not required; archive/revert/local cleanup are preferred.

2. Write predictions before creating anything

Record answers first. You will compare them with evidence later.

Prediction Your expected state before first push Why
Remote project repository Empty We will create the project without README/license/template initialization.
GitLab default branch Likely unset/null until a branch exists No repository branch exists yet; verify rather than assume.
Local main One authoritative commit Local Git already owns the initial history.
Local origin Absent before we add it Project creation does not automatically mutate this local repository in the API path.
Visibility Private Explicit project creation field; never rely on a host default.
Protected branch rule for main Unknown until inspected after branch creation Instance/group defaults can affect protection; do not hard-code expected access levels.

3. Create the authoritative local source state

Use a fresh temporary directory. The main branch and first commit exist before GitLab has any repository history.

LAB_ROOT="$(mktemp -d)"
LOCAL_DIR="$LAB_ROOT/catalog-api"
mkdir -p "$LOCAL_DIR"
cd "$LOCAL_DIR"

git init -b main
printf '# Catalog API\n\nSynthetic Chapter 03 checkpoint.\n' > README.md
printf 'node_modules/\n.env\n' > .gitignore
git add README.md .gitignore
git diff --cached --check
git commit -m "chore: initialize catalog API training project"

LOCAL_SHA_BEFORE="$(git rev-parse HEAD)"
printf 'local_head=%s\n' "$LOCAL_SHA_BEFORE"
git remote -v   # expected: no output

4. Create an empty Private project and prove the pre-push hosted state

Create the project through the REST API using a unique path. Crucially, do not set initialize_with_readme=true.

GITLAB_USER="$(glab api user --jq .username)"
LAB_NAME="catalog-api-ch03-$(date +%Y%m%d%H%M%S)"

FULL_PATH="$(glab api projects -X POST   -f "name=$LAB_NAME"   -f "path=$LAB_NAME"   -f "visibility=private"   -f "description=Disposable Chapter 03 checkpoint"   --jq .path_with_namespace)"
PROJECT_ID="$(glab repo view "$FULL_PATH" -F json --jq .id)"

glab api "projects/$PROJECT_ID"   --jq '{id,path_with_namespace,visibility,default_branch,empty_repo,archived}' 

Compare output to your prediction. On an empty project, a null/unset default branch is legitimate. Do not “fix” it by clicking Initialize README; the local repository is the declared source of truth.

5. Connect the exact GitLab project and push the first branch

Ask GitLab for the clone URL instead of typing the namespace from memory. The example uses SSH because Chapter 02 established an SSH path; if your glab configuration uses HTTPS, use the documented HTTPS URL and your credential manager.

SSH_URL="$(glab api "projects/$PROJECT_ID" --jq .ssh_url_to_repo)"
git remote add origin "$SSH_URL"

git remote -v
git status --short --branch

# First push: creates refs/heads/main on GitLab.
git push -u origin main

LOCAL_SHA="$(git rev-parse HEAD)"
REMOTE_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
printf 'local=%s\nremote=%s\n' "$LOCAL_SHA" "$REMOTE_SHA"
test "$LOCAL_SHA" = "$REMOTE_SHA" && echo 'first push identity verified' 

6. Prove hosted-versus-Git state after the first push

Now inspect the GitLab project again. The branch exists and GitLab can designate a default branch. Verify, do not assume the exact protection rule inherited on your account.

glab api "projects/$PROJECT_ID"   --jq '{path_with_namespace,visibility,default_branch,empty_repo,topics,archived}'

glab api "projects/$PROJECT_ID/protected_branches"   --jq '.[] | {name,allow_force_push,push_access_levels,merge_access_levels}'

git branch -vv
git log --oneline --decorate --graph --all -n 5

Causality: the API project existed before any Git ref. The first push created refs/heads/main on the remote. The commit SHA matches because the remote received the exact local Git object graph. GitLab then exposes that ref as project/default-branch state and may apply configured protection policy around it.

7. Make one hosted-only change and prove Git is unchanged

Capture the commit SHA, update the description, and compare again.

HEAD_BEFORE_METADATA="$(git rev-parse HEAD)"

glab repo update "$FULL_PATH"   --description "Disposable Chapter 03 checkpoint — hosted metadata changed"

glab repo view "$FULL_PATH" -F json --jq '{description,visibility,default_branch}'
test "$HEAD_BEFORE_METADATA" = "$(git rev-parse HEAD)" &&   echo 'hosted metadata changed without a Git commit' 

8. Required failure drill: wrong remote, correct identity

Break only the local target. This tests whether you can distinguish authentication success from project-path authorization/targeting.

GOOD_ORIGIN="$(git remote get-url origin)"
git remote set-url origin "git@gitlab.com:synthetic-missing/ch03-checkpoint.git"

set +e
git ls-remote origin
RC=$?
set -e
printf 'broken_remote_exit=%s\n' "$RC"

# Diagnose: authentication may still be valid; target is wrong.
glab auth status
printf 'expected_project=%s\n' "$FULL_PATH"

# Least destructive repair.
git remote set-url origin "$GOOD_ORIGIN"
git ls-remote --heads origin

Do not create a broader token, request Owner, disable protection, or force-push. None of those actions correct a wrong remote URL.

9. Fork/template/blank-project decision review

Need Blank empty project Fork Template-derived project
Existing local Git history is source of truth Best fit Usually wrong relationship Could inject unrelated/extra starter history.
Contribute changes back upstream No upstream semantics Best fit No ongoing upstream relationship.
Standardized starter content Manual/reviewed bootstrap Not the purpose Best fit; built-in across tiers, group custom templates Premium/Ultimate.
Independent long-term project Yes Fork relationship remains meaningful Yes.
Free mandatory path Yes Yes, with suitable destination namespace Built-in templates yes; custom group templates no.

10. Create a compact evidence packet

Evidence should contain identifiers and state, not credentials. Save only synthetic project metadata and Git identities.

mkdir -p "$LAB_ROOT/evidence"

glab api "projects/$PROJECT_ID"   --jq '{id,path_with_namespace,visibility,default_branch,archived,empty_repo,topics,web_url}'   > "$LAB_ROOT/evidence/project.json"

glab api "projects/$PROJECT_ID/protected_branches"   --jq '[.[] | {name,allow_force_push,push_access_levels,merge_access_levels}]'   > "$LAB_ROOT/evidence/protected-branches.json"

git rev-parse HEAD > "$LAB_ROOT/evidence/local-head.txt"
git ls-remote origin refs/heads/main > "$LAB_ROOT/evidence/remote-main.txt"
git remote -v > "$LAB_ROOT/evidence/remotes.txt"

# Inspect for accidental secrets before sharing evidence.
grep -RniE 'glpat-|PRIVATE-TOKEN|Authorization:|password=' "$LAB_ROOT/evidence" ||   echo 'no obvious credential strings found' 

11. Verification checklist

  • The project path is under the intended disposable namespace.
  • Visibility is Private.
  • The hosted project was empty before the first push.
  • Local main had one authoritative root before GitLab received a ref.
  • origin points to the intended project, not merely a same-named project.
  • Local HEAD equals remote refs/heads/main after the push.
  • The project API reports the expected default branch after branch creation.
  • Protected-ref state was inspected, not guessed.
  • Description changed without changing Git HEAD.
  • The intentionally broken remote was repaired without changing credentials or Git history.

12. Cleanup / rollback and project disposition

The safest course cleanup is to keep the synthetic evidence, remove the local working clone, and either leave the private disposable project for Chapter 04 or archive it after verifying the exact path. Deletion is not required.

# Verify target before any hosted cleanup.
glab repo view "$FULL_PATH" -F json   --jq '{path_with_namespace,visibility,archived,web_url}'

# Optional reversible hosted disposition after you are completely finished:
# glab repo update "$FULL_PATH" --archive=true

# Remove only the disposable local lab tree after copying evidence you want to keep.
cd /
# rm -rf -- "$LAB_ROOT"
Destructive option deliberately omitted from the required steps. If you choose project deletion, use only this disposable project, understand the current pending-deletion/restore behavior for your offering/version, export evidence first, and verify the exact project path before confirming.

Knowledge check

Before the first push, the project API reports an empty repository and no default branch. Is that a failure?

After changing only the GitLab project description, local HEAD changed. What should you conclude?

The failure drill returns “project not found” while glab auth status is healthy. What is the first correction?

Why did the checkpoint avoid a custom group project template?

What should the next chapter add to this project model?

Chapter 03 summary

You can now treat GitLab project creation as architecture rather than form filling. The project is a namespace-scoped governance boundary around Git history; creation method determines initial history and upstream relationships; visibility determines exposure; paths become automation interfaces; protected refs wrap Git refs with hosted policy; archive/delete/transfer have distinct blast radii; and every mutation should be preceded and followed by evidence. This is the foundation Chapter 04 needs to reason about membership and inherited authorization.

Official references

Next chapter

Groups, subgroups, membership, roles, and access governance

Chapter 04 moves from “what project did we create?” to “which identities receive which effective permissions through namespace inheritance, direct membership, sharing, and custom roles?”

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.