Signal9Signal9 — home
online
all lessons

Git & CI/CD Pipelines

Intermediate45 min6 steps
What you'll learn
  • 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
  1. 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 main stays 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 status
    Cloning 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?

  2. 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/login
    Auto-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.

  3. 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?

  4. 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 a Jenkinsfile — 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.yml
    name: 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 test
    Read 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.yml
      deploy:
        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?

  5. 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?

  6. 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 --oneline
    A 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 --graph
    Branch, 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.
Recap
  • 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 → command / concept
TaskHow
Copy a repogit clone URL
Isolate workgit checkout -b feature
Snapshotgit add PATH · git commit -m 'why'
Publish / trigger CIgit push -u origin feature
Combine / resolvegit merge X → edit markers → add + commit
Gate a stageneeds: [test]
Deploy from main onlyif: github.ref == 'refs/heads/main'
Use a secret${{ secrets.NAME }} — never in code; rotate if ever committed