Git & CI/CD Pipelines
- Explain what Git actually stores — snapshots, not diffs — and why branching is cheap
- Use Git for everyday version control: clone, branch, commit, merge, and conflict resolution without fear
- Explain what CI/CD buys — minutes-not-weeks feedback — and the anatomy of a pipeline
- Read and write a basic CI workflow (build → test → gate → deploy)
- Handle secrets and deployment strategies responsibly, including AI-assisted code review discipline
Git — what it actually is, before what it does
Build the snapshot mental model that makes every Git command predictable
Everything in modern software delivery — code review, CI, automated deploys — is machinery bolted onto Git, so the mental model comes first. And the model is simpler than Git's reputation: Git is a time machine built from snapshots. Every commit is a complete photograph of your project at one moment, plus a message saying why, plus a pointer to the photo before it. Not diffs — photographs (Git computes diffs on demand by comparing photos). History is just the chain of photographs.
A branch is nothing more than a movable label pointing at one of those photos. That's why creating a branch is instant and free — you're writing a 41-byte label, not copying the project. It changes how you work: branches aren't a ceremony for big features, they're the disposable workspace for everything, because your experiments live on their own label and
mainstays untouched until a merge brings them in.The everyday loop, which you'll run thousands of times: branch → change → commit → push → pull request → merge.
git clone https://github.com/acme/app.git && cd app git checkout -b feature/login git statusCloning into 'app'... Switched to a new branch 'feature/login' On branch feature/login nothing to commit, working tree clean
- clone URL
- copy a remote repo locally, complete with full history
- checkout -b NAME
- create a branch label and switch to it (newer git: switch -c)
- status
- where am I, what's modified, what's staged — the orientation read
clone copies the whole repository — every photograph ever taken, the full time machine, not just the latest files. checkout -b creates the branch label AND switches to it in one move; from here your commits land on feature/login while main stays frozen. git status is the orientation command you'll run constantly: which branch am I on, what's changed, what's staged. Make it a tic — status before and after everything, until the output is boring. echo 'login form' > login.html git add login.html git commit -m 'add login form skeleton' git push -u origin feature/login[feature/login 3f2a1b9] add login form skeleton 1 file changed, 1 insertion(+) * [new branch] feature/login -> feature/login
- add PATH
- stage changes for the next commit — choose what's in the photo
- commit -m
- take the snapshot with a why-message
- push -u origin BRANCH
- publish the branch; -u links it for future bare 'git push'
The two-step snapshot: add stages what belongs in the next photograph (the staging area lets you commit SOME changes and not others — half-done experiments stay out of the photo), commit takes it with a message future-you will thank you for. push publishes the branch to the shared remote — and that push event is the trigger every CI pipeline in this lesson hangs off. The -u links your local branch to the remote one so future pushes are just 'git push'. Check yourself
Why is creating a Git branch instantaneous even in a repository with 100,000 files?
Merging, and conflicts without fear
Combine branches, and resolve the conflict case calmly
A merge brings one branch's commits into another — your feature photos spliced into main's timeline. Usually it's automatic: Git compares the snapshots and combines changes that touch different lines without asking. The interesting case — the one that scares newcomers unnecessarily — is when both branches changed the same lines. Git has no way to know which version is right, so it stops and asks. That's a conflict, and it's not an error: it's Git correctly refusing to guess.
git checkout main git merge feature/loginAuto-merging app.js CONFLICT (content): Merge conflict in app.js Automatic merge failed; fix conflicts and then commit the result.
Git merged everything it safely could ('Auto-merging'), then stopped at the genuine ambiguity. Inside app.js it has marked the disputed region with three markers: <<<<<<< HEAD, your current branch's version; =======, the divider; >>>>>>> feature/login, the incoming version. Your job is editorial, not technical: open the file, read both versions, write what the code SHOULD say (either version, or a blend), delete the three marker lines, save. git add app.js git commit -m 'merge feature/login: keep new validation, both handlers' git log --oneline -3[main 8c1d2e3] merge feature/login: keep new validation, both handlers 8c1d2e3 merge feature/login: keep new validation, both handlers 3f2a1b9 add login form skeleton 9a8b7c6 fix header layout
add marks the conflict resolved (yes, the same add as staging — 'this file's current content is correct'), commit completes the merge, and the log shows the timelines joined. The whole ritual: read both sides, decide, delete markers, add, commit. Conflicts stop being scary the day you've resolved three of them on purpose — the practice step arranges exactly that. Check yourself
During a merge, Git says CONFLICT in config.js. Describe the complete resolution sequence.
What CI/CD actually is
Understand what the pipeline buys and how its stages gate each other
Before automation, integration was an event — weeks of parallel work slammed together at the end, breaking in ways nobody could untangle, followed by a manual deploy performed from a checklist by the one person who knew the incantations. The insight of CI (Continuous Integration) is that integration pain scales with time between integrations — so integrate every push: build it, test it, within minutes, automatically. A bug introduced at 2:14 is reported at 2:19, while the change is one commit and the context is fresh. CD (Continuous Delivery/Deployment) extends the same automation through shipping: the tested build flows to staging (and, with full deployment, production) with no human typing deploy commands from memory.
A pipeline is the assembly line that does it: stages in sequence, each one gating the next:
`push → build → test → security scan → deploy staging → deploy prod
`Any stage fails, the line stops, nothing downstream runs — bad code physically cannot reach production through the pipeline. The green checkmark isn't decoration; it's a proof: this exact commit built, passed every test, and cleared the scans.
Check yourself
A team merges feature branches monthly and integration week is legendarily painful. What's the CI argument for merging daily instead?
A CI workflow, read top to bottom
Read the YAML: triggers, jobs, steps, gates
Pipelines are defined in a file in the repo — versioned, reviewed, and branched exactly like code (infrastructure as code again — the automation lesson's idea, applied to delivery). On GitHub it's
.github/workflows/*.yml; GitLab uses.gitlab-ci.yml, Jenkins aJenkinsfile— the shape is universal even where syntax differs: a trigger, then jobs made of steps.Example — not runnable on your lab box
cat .github/workflows/ci.ymlname: CI on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npm testRead it as three layers. WHEN (on:): every push to main and every pull request — the PR trigger is what puts red/green checks on reviews. WHERE (runs-on): a FRESH Ubuntu VM per run — nothing survives from last time, which is a feature: the pipeline proves the project builds from scratch, not just on machines with lucky leftovers. WHAT (steps): check out the code (a fresh VM has nothing — even that is explicit), install the toolchain, install deps, test. If any step exits non-zero the job fails — exit codes as truth, one more time. Example — not runnable on your lab box
cat .github/workflows/deploy-job.ymldeploy: needs: [test] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./deploy.sh env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}- needs: [test]
- gate — run only after listed jobs succeed
- if: github.ref == …
- condition — e.g. deploy from main only, never from PRs
- ${{ secrets.NAME }}
- inject from the encrypted secret store at runtime
The deploy job shows the three safety mechanisms in four lines. needs: [test] is the GATE — deploy cannot start unless test passed; this dependency chain IS the assembly line. The if: restricts deploys to main — pull requests get tested but never shipped. And secrets.DEPLOY_KEY injects the credential from the platform's encrypted store at runtime — the workflow file, which is public code in the repo, never contains it. Speed comes from the same vocabulary: independent jobs (lint, test, scan) with no needs between them run in PARALLEL; only ordering constraints serialize. Check yourself
Why do CI jobs run on a fresh VM every time, when reusing a warm machine would be faster?
Deploy safely: secrets, strategies, and AI-assisted code
Ship without leaking credentials or causing unrecoverable outages
Automating deployment concentrates risk: the pipeline holds production credentials and pushes code at production machines. Two disciplines keep that safe.
Secrets never touch the repo. Not in code, not in the workflow file, not in a config "just temporarily." Git's photographs are forever — a key committed once lives in history even after you delete it, visible to anyone who ever clones; the only remedy is rotating the credential (assume leaked, reissue). Secrets live in the platform's encrypted store — repo secrets, Vault, a cloud secret manager — injected as environment variables at runtime, scoped per environment (staging keys can't touch prod), rotated on schedule.
Deployment strategy is rollback strategy. The naive deploy — overwrite everything at once — has no undo; if the new version is broken, you're down until a fix ships. The three standard strategies all buy the same thing, reversibility, at different granularity:
| Strategy | How | Rollback |
|----------|-----|----------|
| Rolling | Replace instances a few at a time | Stop rolling; replace back |
| Blue/Green | Full parallel environment, switch traffic | Switch traffic back — seconds |
| Canary | New version gets 5% of traffic, watch metrics, widen | Route the 5% back |
The question that picks one isn't "how do we deploy?" — it's "when this version is bad, how fast can we be back on the old one?" Automated deploys are only safe because they're automatedly undoable.
Check yourself
A teammate committed an API key last week, then deleted it in a follow-up commit. Is the key safe, and what's the required action?
Now do it — run the loop and stage a conflict on purpose
Drive branch → commit → merge for real, including a deliberate conflict resolution
Two exercises: the everyday loop until it's rhythm, then the rite of passage — a merge conflict you engineer deliberately, so your first one happens in a throwaway repo instead of a deadline. Type these; the muscle memory is the point.
cd /tmp && rm -rf gitdemo && git init gitdemo && cd gitdemo echo 'hello' > file.txt git add file.txt git -c user.email=you@x -c user.name=you commit -m 'first commit' git log --onelineA fresh repo and the core rhythm: create, stage, snapshot, read history. The -c flags supply an identity inline for this throwaway (real machines set it once with git config --global). One commit, one photograph — the chain starts. git checkout -b feature echo 'feature line' >> file.txt git -c user.email=you@x -c user.name=you commit -am 'feature work' git log --oneline --all --graphBranch, change, commit (-am stages modified tracked files and commits in one step). The --graph log draws the fork: two labels, diverging timelines. This picture — main here, feature there — is the one to hold in your head during every merge you'll ever run. git checkout main sed -i.bak 's/hello/hello MAIN/' file.txt && rm -f file.txt.bak git -c user.email=you@x -c user.name=you commit -am 'main changes line 1' git checkout feature sed -i.bak 's/hello/hello FEATURE/' file.txt && rm -f file.txt.bak git -c user.email=you@x -c user.name=you commit -am 'feature changes line 1'The setup: both branches now change the SAME line differently — main says 'hello MAIN', feature says 'hello FEATURE'. Git will have no way to pick. You've engineered the exact situation that panics newcomers, on purpose, in /tmp where nothing matters. git checkout main && git merge feature; cat file.txt # edit file.txt: keep the line you want, DELETE the <<<<<<< ======= >>>>>>> markers, then: # git add file.txt && git -c user.email=you@x -c user.name=you commit -m 'resolved'There it is: CONFLICT, and cat shows the three markers fencing both versions. Now the calm ritual — open it in your editor, choose (or blend), delete all three marker lines, add, commit. Run git log --oneline --graph afterward and watch the timelines join. That's the whole monster: an edit and two commands. You've now resolved a merge conflict; the next one is just Tuesday.
- Git is snapshots, not diffs: a commit is a full photograph + message + parent; a branch is a free movable label. The loop: branch → change → add/commit → push → PR → merge.
- A conflict is Git refusing to guess when both branches changed the same lines: read both versions between the <<<<<<< ======= >>>>>>> markers, edit to the right answer, delete markers (grep for strays), add + commit.
- CI = build + test on every push (bugs reported in minutes, while the change is one commit); CD = automated, repeatable shipping. Pipelines are staged assembly lines where each stage gates the next — order them cheap-to-expensive to fail fast.
- Workflow files live in the repo (versioned like code): on = trigger, jobs on fresh VMs (clean-slate reproducibility), needs = gate, if = restrict deploys to main, ${{ secrets.* }} = runtime injection.
- Secrets never touch the repo — Git history is forever, so a committed key is compromised until ROTATED. Deployment strategy = rollback strategy (rolling / blue-green / canary all buy reversibility). Review AI-generated code with suspicion; the pipeline backstops review, never replaces it.
| Task | How |
|---|---|
| Copy a repo | git clone URL |
| Isolate work | git checkout -b feature |
| Snapshot | git add PATH · git commit -m 'why' |
| Publish / trigger CI | git push -u origin feature |
| Combine / resolve | git merge X → edit markers → add + commit |
| Gate a stage | needs: [test] |
| Deploy from main only | if: github.ref == 'refs/heads/main' |
| Use a secret | ${{ secrets.NAME }} — never in code; rotate if ever committed |
