Git in CI/CD, Infrastructure as Code, Release Automation, and GitOps: Guided Hands-On Workflow and Core Operations
Create an account-free local delivery system with a bare server, shallow/no-tags runner, exact detached build, provenance report, history/tag fetch, guarded non-force release bot, and file-based desired-state reconciliation.
Learning objectives
- Simulate a shallow/no-tags CI clone and diagnose missing release context without losing commit provenance.
- Build from an exact detached commit and record commit/tree/clean-state evidence.
- Fetch only the history/tags required for robust version context.
- Update an automation-owned release branch with normal fast-forward semantics and handle a race without force.
- Advance applied state only after desired-state validation succeeds.
1. Build a local CI/CD and GitOps laboratory
Everything stays on the local filesystem. The “server” is a bare Git repository; source, runner, and bot are ordinary clones. No cloud account, CI product, Kubernetes cluster, or credentials are required.
Git Bash / Bash / zsh
mkdir git-automation-lab
cd git-automation-lab
git init --bare -b trunk server.git
git clone server.git source
git -C source config user.name "Learner Example"
git -C source config user.email "learner@example.invalid"
PowerShell
New-Item -ItemType Directory git-automation-lab | Out-Null
Set-Location git-automation-lab
git init --bare -b trunk server.git
git clone server.git source
git -C source config user.name "Learner Example"
git -C source config user.email "learner@example.invalid"
2. Create source, desired state, tests, and release history
mkdir -p source/app source/environments/lab
printf "message=hello\n" > source/app/app.conf
printf "replicas=2\nversion=1\n" > source/environments/lab/service.env
cat > source/validate.sh <<'EOF'
#!/bin/sh
replicas=$(sed -n 's/^replicas=//p' environments/lab/service.env)
version=$(sed -n 's/^version=//p' environments/lab/service.env)
test -n "$replicas" && test "$replicas" -ge 1 && test -n "$version"
EOF
git -C source add .
git -C source commit -m "seed application and desired state"
git -C source tag -a v1.0 -m "release v1.0"
printf "message=hello-ci\n" > source/app/app.conf
git -C source add app/app.conf
git -C source commit -m "update application message"
printf "replicas=3\nversion=2\n" > source/environments/lab/service.env
git -C source add environments/lab/service.env
git -C source commit -m "scale lab desired state"
git -C source push -u origin trunk
git -C source push origin v1.0
BUILD_OID=$(git -C source rev-parse HEAD)
printf "build_oid=%s\n" "$BUILD_OID"
The annotated tag is deliberately two commits behind the tip. This lets a shallow no-tags runner demonstrate why release-description logic may need more history and tags.
3. Create a shallow/no-tags CI-like clone over file://
SERVER_URL="file://$(pwd)/server.git"
git clone --depth=1 --no-tags --branch trunk "$SERVER_URL" runner
git -C runner rev-parse --is-shallow-repository
git -C runner status --short --branch
git -C runner tag --list
git -C runner log --oneline --decorate --all
file:// is intentional: Git can ignore
--depth for a direct local-path optimization. Using the
file transport makes the lab exercise actual shallow-clone behavior.
4. Observe missing version context before “fixing” it
git -C runner describe --tags --exact-match HEAD
echo "exact tag exit=$?"
git -C runner describe --tags HEAD
echo "describe exit=$?"
git -C runner rev-parse --verify HEAD^{commit}
Expected: both describe commands fail because the old tag/history are absent, while the exact commit ID remains available. The build can still have correct commit provenance even when human-readable release context is incomplete.
5. Perform an exact-commit detached checkout
test "$(git -C runner rev-parse HEAD)" = "$BUILD_OID"
git -C runner switch --detach "$BUILD_OID"
git -C runner branch --show-current
git -C runner rev-parse --symbolic-full-name HEAD
git -C runner rev-parse --verify HEAD^{commit}
The branch-name command should output nothing. The exact commit
remains equal to BUILD_OID.
6. Generate provenance before the build
cd runner
COMMIT=$(git rev-parse --verify HEAD^{commit})
TREE=$(git rev-parse --verify HEAD^{tree})
SHORT=$(git rev-parse --short HEAD)
if test -z "$(git status --porcelain=v1)"; then CLEAN=true; else CLEAN=false; fi
printf "commit=%s\ntree=%s\nshort=%s\nclean=%s\n" \
"$COMMIT" "$TREE" "$SHORT" "$CLEAN" | tee provenance.txt
sh validate.sh
test "$CLEAN" = true
cd ..
The validator is project-specific; the provenance fields are Git facts. In production, record the toolchain/runtime identity alongside them.
7. Create an artifact from the exact commit rather than the branch name
git -C runner archive --format=tar \
-o ../build-"$(git -C runner rev-parse --short HEAD)".tar HEAD
git archive reads the named tree, so this artifact
corresponds to the detached commit. As Chapter 24 established, this
tarball is a source artifact, not a replacement for Git history.
8. Fetch history and tags only because version derivation requires them
git -C runner fetch --unshallow origin
git -C runner fetch --tags origin
git -C runner rev-parse --is-shallow-repository
git -C runner tag --list
git -C runner describe --tags --always HEAD
Expected: the runner becomes complete, receives v1.0,
and describe can now express HEAD relative to that
release. The exact commit ID must remain unchanged throughout.
9. Optional: use a detached worktree for parallel exact-commit testing
git -C source worktree add --detach ../secondary-build "$BUILD_OID"
git -C secondary-build rev-parse HEAD
git -C secondary-build branch --show-current
git -C source worktree list --porcelain
git -C source worktree remove ../secondary-build
Linked worktrees share the repository's object database and most refs but have independent HEAD/index/worktree state. They are useful for local build orchestration without repeatedly switching the developer worktree.
10. Create a dedicated release/config branch
git -C source switch -c release-config
printf "release_commit=%s\nstatus=approved\n" "$BUILD_OID" > source/release.env
git -C source add release.env
git -C source commit -m "initialize release configuration"
git -C source push -u origin release-config
git -C source switch trunk
The release branch begins from the source history but has a separate purpose and ownership policy. A production repository may instead use a dedicated repository/ref namespace; the important control is that automation does not rewrite human development history.
11. Clone a bot workspace with metadata identity but no real credential
git clone "$SERVER_URL" bot
git -C bot config user.name "Release Automation"
git -C bot config user.email "release-bot@example.invalid"
git -C bot switch release-config
git -C bot status --short --branch
git -C bot log -3 --oneline
git -C bot remote -v
The local filesystem remote needs no credential. In production, bot authorization comes from server permissions/credential material—not from the name/email above.
12. Make a normal fast-forward bot update
printf "release_commit=%s\nstatus=released\n" "$BUILD_OID" > bot/release.env
git -C bot add release.env
git -C bot commit -m "mark build as released"
git -C bot fetch origin release-config
REMOTE_BEFORE=$(git -C bot rev-parse refs/remotes/origin/release-config)
git -C bot merge-base --is-ancestor "$REMOTE_BEFORE" HEAD
git -C bot push --dry-run --porcelain origin HEAD:refs/heads/release-config
git -C bot push origin HEAD:refs/heads/release-config
No force option is used. The bot proves the remote-tracking tip is an ancestor of its proposed commit, previews the push, then performs a normal fast-forward update.
13. Simulate a race and let normal push protection stop the stale bot
git clone "$SERVER_URL" operator
git -C operator config user.name "Release Operator"
git -C operator config user.email "operator@example.invalid"
git -C operator switch release-config
printf "operator_note=hold\n" > operator/release-hold.env
git -C operator add release-hold.env
git -C operator commit -m "operator places release hold"
git -C operator push origin release-config
printf "automation_note=publish-metadata\n" > bot/automation-note.env
git -C bot add automation-note.env
git -C bot commit -m "automation adds metadata"
git -C bot push origin HEAD:refs/heads/release-config
echo "stale bot push exit=$?"
Expected: the bot push is rejected as non-fast-forward. That rejection protects the operator's newer work.
14. Repair the race without force
git -C bot fetch origin release-config
git -C bot merge --no-edit refs/remotes/origin/release-config
git -C bot push --dry-run --porcelain origin HEAD:refs/heads/release-config
git -C bot push origin HEAD:refs/heads/release-config
The bot merges the new remote tip, preserving both concurrent changes and producing a fast-forwardable update. A production bot could instead recompute its generated commit from the latest remote state; the important rule is that it does not overwrite unseen human work.
15. Build a local GitOps-style reconciler with separate applied state
mkdir deployed-state
printf "none\n" > deployed-state/applied-commit.txt
DESIRED=$(git -C runner rev-parse HEAD)
APPLIED=$(cat deployed-state/applied-commit.txt)
printf "desired=%s\napplied=%s\n" "$DESIRED" "$APPLIED"
if test "$DESIRED" != "$APPLIED"; then
sh runner/validate.sh
cp runner/environments/lab/service.env deployed-state/service.env
printf "%s\n" "$DESIRED" > deployed-state/applied-commit.txt
fi
printf "after reconcile applied=%s\n" "$(cat deployed-state/applied-commit.txt)"
The marker lives outside the Git source checkout. It changes only after validation/application succeeds.
16. Simulate a failed desired state without lying about applied state
git -C source switch trunk
printf "replicas=0\nversion=3\n" > source/environments/lab/service.env
git -C source add environments/lab/service.env
git -C source commit -m "bad desired state for lab"
git -C source push origin trunk
BAD_OID=$(git -C source rev-parse HEAD)
git -C runner fetch origin trunk
git -C runner switch --detach "$BAD_OID"
BEFORE_APPLIED=$(cat deployed-state/applied-commit.txt)
sh runner/validate.sh
echo "validation exit=$?"
AFTER_APPLIED=$(cat deployed-state/applied-commit.txt)
test "$BEFORE_APPLIED" = "$AFTER_APPLIED"
Expected: validation fails because replicas is zero, and the applied marker does not advance. The Git commit is still valid desired-source history; it simply did not become successful applied state.
17. Document rollback as a state transition, not a Git myth
Two common production choices are: revert/fix desired state in Git so the reconciler applies a new commit, or instruct the deployment system to re-apply a previously validated artifact/commit while a source fix is prepared. The exact workflow depends on the deployment platform. Never rewrite the applied marker to claim success that did not occur.
18. Challenge — choose the evidence/operation
- Which command proves the exact commit independently of branch state?
-
What should a build do if
git describefails butHEAD^{commit}is valid? - What operation should a stale release bot attempt before considering any force?
- Where should the lab record successful applied state to avoid confusing it with desired source state?
- Which artifact operation can export exactly the detached commit's tree?
19. Cleanup
cd ..
pwd
rm -rf git-automation-lab
PowerShell equivalent:
Set-Location ..; Remove-Item -Recurse -Force
git-automation-lab.
20. Knowledge check
Question 1. Why did the first
describe fail?
Question 2. What changed when the runner detached HEAD?
Question 3. Why was the stale bot push rejection desirable?
Question 4. Why did the bad desired-state commit remain in Git?
Question 5. Why is the release bot's email not a credential?
21. Summary
You built an account-free CI/CD/GitOps simulation: shallow no-tags runner, exact detached build, commit/tree/clean provenance, explicit history/tag fetch, detached worktree, dedicated release branch, non-force bot update with race rejection/rebase, and reconciliation that advances applied state only after validation.
Authoritative references
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.