The security audit in 08-03 changed twelve things in the toolkit, and the optimization lesson changed just as many before that. Right now, if watchdog.sh starts failing on Tuesday at dawn, nobody can say what was touched, when or why, nor how to get back to the version that worked. This lesson puts ~/veloz-ops/ under version control. It is not a Git course: it is Git seen from the concrete need of an operations team that maintains five scripts, a library and a configuration file with credentials that must never be pushed.
Contents
- Why
backup.sh.bak.old.v2is not version control - The five concepts you need
- Putting
~/veloz-ops/under control - Seeing what changed:
git diffandgit log - What is NOT versioned:
.gitignoreand the configuration - If you already pushed a secret
- Commit messages that are useful in operations
- Undoing with judgment
- Branches for testing without touching production
- Tags and deploying to the fleet
- Remotes and private repositories
- A real
pre-commithook - A minimal workflow for a small team
- Why
backup.sh.bak.old.v2 is not version control
backup.sh.bak.old.v2 is not version controlThe pattern is universal: before touching a script you make a copy with a suffix. A year later the directory holds backup.sh.bak, backup.sh.old, backup.sh.v2, backup.sh.20260715 and backup.sh.GOOD. And none of those copies answers the questions that matter: what exactly changed between any two of them, who changed it, when, why and which one is running right now on srv-veloz-02. Nor do they let you know whether Tuesday's change and Thursday's are independent.
Git answers all five questions and adds two capabilities the copies do not give you: testing a change in parallel without touching what is in production, and undoing one specific change without undoing the later ones.
- The five concepts you need
| Concept | What it is | Analogy |
|---|---|---|
| Repository | The directory with its complete history, in .git/ |
The project archive |
| Staging area | An intermediate zone where you choose what goes into the next commit | The tray of what you are about to hand in |
| Commit | A snapshot of the project with author, date and message | A signed logbook entry |
| Branch | An independent line of development | A parallel draft |
| Remote | A copy of the repository somewhere else | The server where the team shares |
The staging area is what confuses people most at first and it has a very practical reason: it lets you touch four files and make two separate commits, one with the security fix and another with the performance improvement. In operations that is worth gold, because it lets you revert one without the other.
- Putting
~/veloz-ops/ under control
~/veloz-ops/ under control$ cd ~/veloz-ops
$ git init
Initialized empty Git repository in /home/veloz/veloz-ops/.git/
$ git config user.name "Ana Lopez"
$ git config user.email "alopez@velozenvios.example"Before the first git add you have to decide what does not go in, so the right thing is to write the .gitignore first (section 5) and then add:
$ git add .gitignore bin/ lib/ etc/veloz-ops.conf.example
$ git status --short
A .gitignore
A bin/daily-report.sh
A bin/watchdog.sh
A etc/veloz-ops.conf.example
A lib/common.sh
$ git commit -m "Initial version of the Veloz Envios operations toolkit"
[master (root-commit) 4f2a1b8] Initial version of the operations toolkit
8 files changed, 1247 insertions(+)git add moves changes to the staging area; git commit records them in the history. In git status --short, A is added, M modified, D deleted and ?? untracked. One detail that saves surprises: Git stores the execute bit, so the chmod +x of bin/ travels with the repository.
- Seeing what changed:
git diff and git log
git diff and git log$ git diff # UNSTAGED changes (working tree vs. staging)
$ git diff --staged # staged changes (staging vs. last commit)
$ git log --oneline --graph --decorate -4
* 9c14e7a (HEAD -> master, tag: v1.2) Set PATH and IFS in common.sh (audit)
* 3b8d201 Replace loop with awk in daily-report.sh: 3m12s -> 3.9s
* a71f0c4 Add --dry-run to backup.sh
* 4f2a1b8 Initial version of the toolkitThe distinction between the two diff commands is the most asked question when starting out: the first shows what you have not yet added with git add; the second, what is about to go into the commit. Always review git diff --staged before writing the message: it is the natural moment to discover you left a set -x in place. To investigate a particular file, git log -p bin/watchdog.sh shows its history line by line and git blame bin/watchdog.sh says which commit introduced each line: the tool you will really use when something has been broken for months.
- What is NOT versioned:
.gitignore and the configuration
.gitignore and the configurationThis is the critical part. etc/veloz-ops.conf holds the token we just protected in 08-03; pushing it makes it visible to the whole team, forever and in every clone.
# ~/veloz-ops/.gitignore
etc/veloz-ops.conf # secrets and local configuration: NEVER
etc/secrets
*.key
*.pem
.netrc
logs/ # regenerable output
*.log
*.tmp
data/ # business data: goes to the backup, not the repository
*.csvInstead, a template is versioned that documents the configuration without revealing anything:
# etc/veloz-ops.conf.example <-- this one IS versioned
VELOZ_API_URL="http://localhost:8080"
VELOZ_API_TOKEN="put-your-token-here" # get it from Vault, do not share
VELOZ_RETENTION_DAYS=30When installing the toolkit on a new server you copy the template, fill it in and adjust permissions. An important warning: .gitignore only affects untracked files. If you already did git add etc/veloz-ops.conf, adding it to .gitignore does not take it out: you need git rm --cached etc/veloz-ops.conf, which removes it from the repository and leaves it on disk.
- If you already pushed a secret
It happens. The order of the actions is not negotiable:
- Rotate the secret immediately. Generate a new one and invalidate the old one. It is the only step that actually mitigates; the rest is cleanup.
- Take it out of the current repository:
git rm --cached, add it to.gitignore, commit. - Rewrite the history with
git filter-repo(today's recommended tool) or BFG Repo-Cleaner, which remove the file from every commit. - Tell the team: the rewrite changes every commit identifier and anyone with a clone will have to redo it.
And the underlying warning: rewriting the history is not enough if the repository has already been shared. Anyone could have cloned it, the platform could have indexed it, there are copies in caches and in backups. That is why step 1 is neither optional nor later.
- Commit messages that are useful in operations
A message does not describe the diff — the diff already does that — but what changed and why. In operations, the "why" is usually an incident:
| Useless message | Useful message |
|---|---|
changes |
Add a 10s timeout to veloz_api_get |
fix |
Fix division by zero in veloz_percentage with no shipments |
update watchdog.sh |
Raise the disk threshold to 85% to avoid false alerts |
wip |
Set PATH and IFS in common.sh (security audit) |
Practical convention: an imperative first line under 72 characters, a blank line and a body with the context.
$ git commit -m "Retry the veloz-api query up to 3 times" -m \
"Incident INC-482: on 2026-07-29 the API took 8s to respond after the
nightly restart and watchdog.sh sent a false alert. Retries with
exponential backoff (2s, 4s, 8s) are added before alerting."Six months from now, that commit explains why a retry loop exists that at first sight looks unnecessary. One commit per logical change: do not mix the performance refactor with the security fix even if you did both the same afternoon.
- Undoing with judgment
Four commands for four situations. Confusing them is the main source of lost work:
| Situation | Command | Destructive |
|---|---|---|
| Discard my edits to a file | git restore bin/watchdog.sh |
Yes (loses unsaved work) |
| I staged too much | git restore --staged etc/veloz-ops.conf |
No |
| An already published commit is wrong | git revert 3b8d201 |
No (creates an inverse commit) |
| Drop local commits not yet published | git reset --hard 4f2a1b8 |
Yes, it deletes history |
Golden rule: git revert for what others have already seen; git reset --hard only for your own, unpublished work. revert leaves a record that there was a change and that it was withdrawn, which in operations is valuable information. And if the last commit's message is wrong, git commit --amend fixes it as long as you have not pushed it.
- Branches for testing without touching production
You want to rewrite watchdog.sh's threshold logic, but that script runs every five minutes on three servers. A branch gives you a parallel space:
$ git switch -c dynamic-thresholds # creates the branch and switches to it
$ git commit -am "Compute the disk threshold from the 7-day average"
$ git switch master # master stays untouchedWhen the change is tested it is integrated with git merge dynamic-thresholds and the branch is deleted with git branch -d. If meanwhile somebody touched the same lines on master, Git reports a conflict and marks the file:
<<<<<<< HEAD
disk_threshold=85
=======
disk_threshold=$(( avg_7days + 10 ))
>>>>>>> dynamic-thresholdsResolving it means editing the file leaving the correct version — often a combination of both — deleting the three marker lines and running git add and git commit. There is no magic: a human decides. With Bash scripts it is also worth running bash -n (05-03) on the resolved file before committing, because a badly resolved conflict easily leaves one fi too many.
- Tags and deploying to the fleet
A tag marks a point in history with a stable name, and in operations it tells you which version is running on each server:
Deploying to srv-veloz-01/02/03 combines this with what we saw in 07-06:
# A) Each server has a clone and updates to the tag
for h in srv-veloz-01 srv-veloz-02 srv-veloz-03; do
ssh -n "$h" 'cd ~/veloz-ops && git fetch --tags && git checkout -q v1.2'
done
# B) A reference clone and rsync to the fleet (no Git in production)
for h in srv-veloz-01 srv-veloz-02 srv-veloz-03; do
rsync -a --delete --exclude '.git' --exclude 'etc/veloz-ops.conf' \
~/veloz-ops/ "$h:~/veloz-ops/"
doneOption A lets you query the deployed version with git describe on each machine; option B avoids installing Git and credentials in production. Note the exclusion of etc/veloz-ops.conf: the configuration is local to each server and must not be overwritten from the repository.
- Remotes and private repositories
A local repository is not a backup: if the disk is lost, the history is lost.
$ git remote add origin git@git.velozenvios.example:ops/veloz-ops.git
$ git push -u origin master # -u remembers the destination for the next ones
$ git pull # brings in and integrates what others have pushedgit pull is internally fetch plus merge; if you prefer to see what is coming before integrating it, git fetch followed by git log HEAD..origin/master is an excellent habit in operations. And a warning that is not superfluous: operations code goes in private repositories. Even if it contains no secrets — and it must not — it reveals server names, internal paths, thresholds and system structure; it is free reconnaissance for an attacker. Authentication by SSH key (07-06), not by password.
- A real
pre-commit hook
pre-commit hookA hook is a script Git runs automatically at a certain moment. pre-commit runs before creating the commit and, if it exits with a code other than 0, aborts it:
#!/usr/bin/env bash
# .git/hooks/pre-commit - requires chmod +x
set -uo pipefail
failures=0
mapfile -t scripts < <(git diff --cached --name-only --diff-filter=ACM |
grep -E '\.(sh|bats)$|^bin/')
(( ${#scripts[@]} == 0 )) && exit 0
for s in "${scripts[@]}"; do
bash -n "$s" || { printf 'syntax: %s\n' "$s" >&2; failures=1; }
shellcheck -x "$s" || failures=1 # 08-05
done
[[ -d tests ]] && { bats tests/ >/dev/null || failures=1; } # 08-06
if git diff --cached | grep -qE 'TOKEN=|PASSWORD=|BEGIN (RSA|OPENSSH) PRIVATE KEY'; then
printf 'possible secret in the commit, review it\n' >&2
failures=1
fi
exit "$failures"Four barriers: syntax with bash -n, static analysis with ShellCheck, tests with Bats and a simple secret search. --diff-filter=ACM limits the check to added, copied or modified files — there is no point analyzing one that is being deleted — and it only looks at the staged ones, not the whole tree. Two limitations you have to know: hooks live in .git/hooks/ and are not versioned, so they have to be installed in every clone (which is why many teams keep the hook in tools/ and link it with an install script), and git commit --no-verify skips them. The hook is a safety net for slip-ups, not an access control.
- A minimal workflow for a small team
For two or three operations people this is enough: git pull before starting; a short branch for the change (git switch -c fix-api-timeout); small commits with messages that cite the incident; git push -u origin <branch> and a review request to another person; merge to master, tag if it is deployable and deploy to the fleet; delete the branch.
The second-to-last step is the most important on the list, and it is not a tool: it is a person. Review by a colleague is the best quality control there is, better than ShellCheck, better than tests and better than your own discipline. It finds what tools cannot see: that the threshold is badly chosen, that the script assumes a directory that only exists on your machine, that the rm -rf is not as constrained as it looks. Ten minutes of review save a night on call.
Common Mistakes and Tips
- Starting with
git add .and no.gitignore. It is the most common way to push a secret. The.gitignorecomes before the first commit. - Believing
.gitignoreremoves already-tracked files. It does not:git rm --cachedis what takes them out. - Deleting the secret and not rotating it. It is still in the history, in the clones and in the caches.
git reset --hardon already published commits. It breaks everyone else's repository; for what is published,git revert.- Tip:
git stashfor the interruption. It saves half-done work, you handle the incident and get it back withgit stash pop. - Tip:
git log -S 'veloz_api_get'searches for the commit in which a string appeared or disappeared; it is the fast way to date a behavior change.
Exercises
Exercise 1. You discover that etc/veloz-ops.conf, with the API token, has been versioned for three commits in a repository shared with two colleagues. List in order the actions and the concrete commands.
Exercise 2. Write the toolkit's .gitignore, justifying each exclusion, and explain what is versioned instead.
Exercise 3. You have merged into master a change in watchdog.sh (commit 3b8d201) that causes false alerts, and there are already two later commits from a colleague. How do you undo it without losing their work?
Solutions
Solution 1. First, rotate the token in the API and invalidate the old one: it is in three clones and on the Git server, and it is the only thing that stops the real exposure. Then:
$ printf 'etc/veloz-ops.conf\n' >> .gitignore
$ git rm --cached etc/veloz-ops.conf
$ git add .gitignore etc/veloz-ops.conf.example
$ git commit -m "Stop versioning the configuration with credentials"
$ git pushThen rewrite the history with git filter-repo --path etc/veloz-ops.conf --invert-paths (or BFG), warning the two colleagues beforehand that they will have to clone again because every commit identifier changes. And document the incident, remembering that the rewrite does not replace rotation: if the repository was shared, you have to assume the token is public.
Solution 2. The one in section 5. etc/veloz-ops.conf, *.key, *.pem and .netrc contain credentials; logs/ and *.log are regenerable output that would also grow the repository without limit; *.tmp are mktemp temporaries; data/ and *.csv are business data with personal information (08-03), which belong in the backup and not in version control. etc/veloz-ops.conf.example is versioned because it documents which variables have to be defined without revealing their values.
Solution 3. With git revert, which creates a new commit undoing exactly the changes in 3b8d201 and leaves the two later ones untouched:
git reset --hard would be a serious mistake: it would also throw away your colleague's commits and, since they are already published, would force a push --force that would break their repository. If the revert produces a conflict because the later commits touched the same lines, it is resolved by hand as in section 9 and finished with git revert --continue.
Conclusion
For an operations toolkit Git solves what .bak copies do not: what changed, who, when and why, and how to go back without dragging everything else along. Five concepts — repository, staging area, commit, branch and remote — are enough to work: git init, git add, git commit -m, git status --short, git diff and git diff --staged to review before committing, git log --oneline --graph to read the history and git blame to find out who introduced that line. The first thing you write is not a commit but the .gitignore, which leaves out logs/, temporaries, data and above all etc/veloz-ops.conf with its credentials, versioning etc/veloz-ops.conf.example instead; and if a secret is already inside, the order is rotate first, then git rm --cached and then git filter-repo or BFG, knowing that rewriting the history repairs nothing if the repository has already been shared. Messages describe the change and its motive — the incident that caused it — with one commit per logical change. To undo you have to choose well: git restore discards edits, git restore --staged unstages, git revert undoes what is published leaving a record, and git reset --hard is only used on your own unpublished commits. Branches (git switch -c) let you rewrite watchdog.sh without touching what runs every five minutes, with merging and manual conflict resolution; tags (git tag -a v1.2) fix which version is deployed and combine with the fleet deployment from 07-06, either by git checkout on each server or by rsync excluding .git and the local configuration. The remote — private and with an SSH key — is at once a backup and a meeting point. And the pre-commit hook automates the barriers: bash -n, ShellCheck, Bats and a secret search, with the double warning that hooks are not versioned and that --no-verify skips them. Above all of that remains the most effective quality control, which is not a tool: having another person read your change before it reaches production.
The pre-commit hook we just wrote invokes two commands we do not know yet. Lesson 08-05 deals with the first one: ShellCheck, the static analyzer that finds the errors this course has taught you to avoid — and quite a few more — together with shfmt for the uniform formatting that 08-01 left promised. We will go through the most frequent warnings, connecting each one to the lesson where the problem was explained, we will learn to silence a warning with judgment rather than for convenience, and we will run ShellCheck over the five scripts and lib/common.sh to classify and fix everything that shows up.
Bash Programming Course
Module 1: Introduction to Bash
- What Is Bash?
- Setting Up Your Environment
- Basic Command Line Navigation
- Understanding the Shell
- Finding Help: man, help and --help
Module 2: Basic Bash Commands
- File and Directory Operations
- Text Processing Commands
- File Permissions and Ownership
- Redirection and Piping
- Wildcards and Path Expansion
- History and Keyboard Shortcuts
Module 3: Scripting Fundamentals
- Creating and Running a Script
- Variables and Constants
- Basic Operators
- Conditional Statements
- Arguments and User Input
- Quoting, Expansion and Substitution
Module 4: Intermediate Scripting
- Loops in Bash
- Functions in Bash
- Arrays and Associative Arrays
- String Manipulation
- The case Statement and Interactive Menus
- Arithmetic and Numeric Calculations
Module 5: Advanced Scripting Techniques
- Advanced File Operations
- Process Management
- Error Handling and Debugging
- Regular Expressions
- Advanced I/O: Descriptors and Here-Documents
- Modular Scripts and Reusable Libraries
Module 6: Working with External Tools
Module 7: Automation and Scheduling
- Cron Jobs
- Automating Tasks
- Backup and Restore Scripts
- Monitoring and Logging
- Services and Timers with systemd
- Remote Automation with SSH
Module 8: Best Practices and Optimization
- Writing Readable Code
- Optimizing Bash Scripts
- Security Considerations
- Version Control with Git
- Static Analysis with ShellCheck and shfmt
- Automated Testing with Bats
- Portability: POSIX sh versus Bashisms
