Chapter 03Lesson 02~175 minutes

Projects, Repository Settings, Visibility, Forks, Protected Resources, and Project Templates: Guided Hands-On Workflow and Core Operations

Create and inspect a disposable GitLab project, connect it to local Git, verify hosted and repository state independently, and compare blank, template-derived, and fork workflows without relying on paid features.

Disposable labglabProjects APIGit stateMetadataVerification

Learning objectives

  • Create a disposable private project through a free-compatible GitLab UI or API path without accidentally creating a competing Git history.
  • Clone, commit, push, and independently prove the relationship between local Git refs and GitLab project/default-branch metadata.
  • Change safe project metadata only after capturing a before-state and then verify the exact hosted state that changed.
  • Compare blank-project, built-in-template, and fork workflows without requiring Premium/Ultimate or a second namespace.
  • Inspect protected branch/tag state before changing any rule and explain the role/tier boundary for each action.
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.
Lab safety. Use a personal disposable project with synthetic content. Keep it Private. Do not test visibility changes on employer code. The workflow does not create access tokens, runners, deployments, registry images, or cloud resources. glab reuses the authenticated context established in Chapter 02 without printing its credential.

1. Preflight: prove host, identity, and local state before creation

Project creation is a mutation, so first prove which GitLab host and account glab will target. Then generate a unique disposable name so rerunning the lab does not collide with an older project.

glab auth status
GITLAB_USER="$(glab api user --jq .username)"
LAB_NAME="project-governance-lab-$(date +%Y%m%d%H%M%S)"
printf 'user=%s\nproject=%s\n' "$GITLAB_USER" "$LAB_NAME"

# Local preflight: do not run from a valuable repository.
pwd
git rev-parse --show-toplevel 2>/dev/null || printf 'not inside a Git worktree\n' 

If glab auth status names a Self-Managed host instead of GitLab.com, stop and decide whether that instance is an approved training target. Host ambiguity is a governance failure, not a convenience.

2. Create one private project with server-side README initialization

The mandatory path uses the Projects REST API through glab api. That keeps the target and fields explicit and returns machine-readable state. We intentionally initialize a README on the server so this lesson can demonstrate a hosted-first workflow: clone the server history instead of creating a competing local history.

FULL_PATH="$(glab api projects -X POST   -f "name=$LAB_NAME"   -f "path=$LAB_NAME"   -f "visibility=private"   -f "initialize_with_readme=true"   -f "description=Disposable Chapter 03 project governance lab"   --jq .path_with_namespace)"

printf 'created=%s\n' "$FULL_PATH"
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}' 

Expected state: the project is Private, is no longer empty, and has a default branch because README initialization created the first commit. If the project already exists, do not blindly retry the POST; choose another disposable name or inspect the existing project first.

3. Correlate API state with the web UI

Open the project with glab repo view "$FULL_PATH" --web. In the GitLab UI, identify the project namespace/path, visibility, default branch, repository files, and Settings → General surfaces. Do not change anything yet. Your goal is to map human-facing labels to the fields you just observed through the API.

Depending on GitLab version, project features may appear under Visibility, project features, permissions. A Self-Managed administrator can restrict visibility levels or disable features; absence of a control is not automatically a bug.

4. Clone the hosted history, make one local change, and push it

Because the server already owns the first commit, cloning gives the local repository that same ancestry. This avoids the unrelated-history trap.

LOCAL_DIR="${LAB_NAME}-local"
glab repo clone "$FULL_PATH" "$LOCAL_DIR"
cd "$LOCAL_DIR"

printf 'local_head_before=%s\n' "$(git rev-parse HEAD)"
printf 'remote_url=%s\n' "$(git remote get-url origin)"
git branch -vv

printf '\nChapter 03 verification change.\n' >> README.md
git add README.md
git diff --cached --check
git commit -m "docs: add Chapter 03 verification line"

git push origin HEAD

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

The commit changes repository state and therefore appears in Git history. It does not change the project description, visibility, topics, members, or protected-ref rules.

5. Capture before-state, change only safe hosted metadata, and prove after-state

Return to the parent shell directory if desired. We will update the description with glab repo update; this is hosted metadata, so no Git commit should appear.

# Works from anywhere because the project target is explicit.
glab repo view "$FULL_PATH" -F json --jq '{description,visibility,default_branch,topics}'

glab repo update "$FULL_PATH"   --description "Disposable Chapter 03 project — metadata verified"

glab repo view "$FULL_PATH" -F json --jq '{description,visibility,default_branch,topics}'

# Local repository HEAD should be unchanged by a hosted description edit.
cd "$LOCAL_DIR" 2>/dev/null || true
git rev-parse HEAD
Topics are optional in this lab. Current GitLab docs mark Project topics as Beta. You may inspect or assign a synthetic topic such as devops-academy-lab through the UI if the control is available, but do not put confidential business names or customer identifiers in topic names because instance topics can be discoverable.

6. Inspect protected refs before touching policy

Never assume the default branch is unprotected or protected with a specific role combination. Read the current rules first. The default branch is commonly protected by default, but instance/group policy can change the details.

PROJECT_ID="$(glab repo view "$FULL_PATH" -F json --jq .id)"

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

glab api "projects/$PROJECT_ID/protected_tags"   --jq '.[] | {name,create_access_levels}' 

This lesson does not mutate protection. Chapter 08 will go deep on merge governance. Here, the key lesson is that a Git push can be rejected even when authentication succeeded because GitLab policy around the target ref denies the operation.

7. Compare a template-derived project and a fork without requiring paid controls

Free-compatible template path: in Create new → New project/repository → Create from template, inspect the Built-in tab. Built-in templates are part of project creation across tiers. Create another disposable private project only if you want the extra practice, then compare its first commits and files with the blank project.

Fork path: forks are available on all tiers, but a useful fork requires a destination namespace where the fork path is valid. If you own a disposable group or have another permitted namespace, fork a synthetic/public training project. Otherwise use the read-only comparison below rather than creating infrastructure just to satisfy the exercise.

Observation Blank / template-derived project Fork
Git history Independent history created by initialization/template. Copied upstream repository history.
Upstream relationship None unless separately configured. GitLab records a fork relationship to upstream.
Issues/MRs/wiki Independent project state. Not simply cloned from upstream as fork content.
Synchronization expectation No automatic upstream relationship. Designed for contribution/sync patterns with upstream.

8. Challenge: choose the correct surface

For each request, decide whether you would use Git, project settings, the Projects API/glab, or a protected-ref setting before reading the answer:

  1. Change the project description without creating a commit.
  2. Move local branch feature to another commit.
  3. Find whether the server considers the project archived.
  4. Prevent direct pushes to main.
  5. Make a project discoverable to unauthenticated users.

Answers: hosted project metadata; Git; hosted project metadata/API; GitLab protected-branch policy; hosted visibility. Only the second operation is intrinsically a Git ref operation.

9. Cleanup / rollback

Keep the lab project if you want to reuse it in Chapters 04–09. If you are finished, remove the local clone and archive the hosted project rather than deleting it immediately. Archiving is reversible but has important side effects such as removing fork relationships and stopping scheduled pipelines, so inspect before acting.

# From outside the local clone:
cd .. 2>/dev/null || true
rm -rf -- "$LOCAL_DIR"   # only the disposable local clone named above

# Optional hosted cleanup — verify target twice before archiving.
glab repo view "$FULL_PATH" -F json --jq '{path_with_namespace,visibility,archived}'
# glab repo update "$FULL_PATH" --archive=true
# glab repo view "$FULL_PATH" -F json --jq '{path_with_namespace,archived}' 
Do not uncomment archive or deletion commands by habit. Confirm $FULL_PATH names only the disposable project. Project deletion is more destructive and is not required by this lesson.

Knowledge check

Why did this lab initialize the README remotely and then clone, instead of creating an independent local first commit?

The description changed, but git log did not. Is that a failure?

A push is denied although SSH authentication works. What should you inspect next?

Why is a fork not equivalent to copying a repository with git clone and pushing elsewhere?

Summary

The safe workflow is inspect → create one disposable hosted source of truth → clone that history → make a small Git change → compare local and remote SHAs → change hosted metadata separately → inspect protected refs → clean up deliberately. Each surface proves a different class of state, and the learner never needs a paid tier, custom runner, or real production project.

Official references

Next lesson

Choose project defaults deliberately

Lesson 3 turns these mechanics into architecture decisions: visibility, personal versus group ownership, blank versus template/import, fork versus independent copy, and protection strategy.

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.