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.
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.
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
mainhad one authoritative root before GitLab received a ref. -
originpoints to the intended project, not merely a same-named project. -
Local HEAD equals remote
refs/heads/mainafter 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"
Knowledge check
Before the first push, the project API reports an empty repository and no default branch. Is that a failure?
No. That is consistent with an intentionally empty GitLab project. The local repository is the source of truth and the first push will create the first branch/ref.
After changing only the GitLab project description, local HEAD changed. What should you conclude?
Something else changed locally. A project description is hosted metadata and cannot itself create a Git commit.
The failure drill returns “project not found” while
glab auth status is healthy. What is the first
correction?
Restore/verify the exact remote project path. Do not broaden credentials or permissions to fix a targeting error.
Why did the checkpoint avoid a custom group project template?
Mandatory learning must remain Free-compatible. Group custom project templates are Premium/Ultimate, so the chapter teaches their design and copied-state implications without requiring purchase.
What should the next chapter add to this project model?
Chapter 04 adds group/subgroup membership, roles, inheritance, custom roles, and access governance—who can act on the project and why.
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
- GitLab Docs — Create a project
- GitLab Docs — Project and group visibility
- GitLab Docs — Manage projects
- GitLab Docs — Projects API
- GitLab Docs — Forks
- GitLab Docs — Protected branches
- GitLab Docs — Protected tags
- GitLab Docs — Default branch
- GitLab Docs — Project topics
- GitLab CLI — repo commands
- GitLab CLI — repo create
- GitLab CLI — repo update
- GitLab CLI — repo fork
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.