DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    Git & GitHubLesson 12 of 13
    Your progress · 0%
    On this page
    1. Client and server hooks
    2. The pre-commit framework
    3. Scripting GitHub with gh
    4. Your first GitHub Actions workflow
    ↖ Course roadmap
    Lesson 12 · Hooks & automation

    Hooks and automation: let Git check your work

    Run checks automatically: hooks that block secrets and bad commit messages, shared hooks with the pre-commit framework, scripting GitHub with gh, and a first GitHub Actions workflow.

    35 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Client and server hooks

    A hook is a script Git runs at a fixed moment. If a hook that runs before something exits non-zero, Git stops: no commit, no push. That makes hooks the earliest, cheapest place to catch mistakes:

    Hooks give feedback in seconds on your machine. CI on GitHub is the gate nobody can skip.© Diagram: DevOps Academy

    Hooks give feedback in seconds on your machine. CI on GitHub is the gate nobody can skip.© Diagram: DevOps Academy
    HookRunsTypical check
    pre-commitbefore the commit is createdsecrets, formatting, lint on staged files
    commit-msgafter you write the messagemessage format
    pre-pushbefore anything is sentrun the fast tests
    post-merge, post-checkoutafter the operationreinstall dependencies if the lockfile changed
    pre-receive, updateon the server, when a push arrivesreject pushes (self-hosted Git servers)

    On GitHub you don't run server hooks yourself; push protection, rulesets and Actions play that role (the Collaboration and Security lessons).

    Writing hooks

    Hooks live in .git/hooks/, named after the event, and must be executable. Git ships samples ending in .sample. These two are the most useful you can write:

    #!/usr/bin/env bash
    # .githooks/pre-commit: block commits that add something that looks like a secret.
    pattern='AKIA[0-9A-Z]{16}|api[_-]?key[[:space:]]*=[[:space:]]*["'\''][^"'\'']{8,}|-----BEGIN [A-Z ]*PRIVATE KEY-----'
    if git diff --cached -U0 | grep '^+' | grep -qiE "$pattern"; then
      echo "pre-commit: this commit seems to add a secret. Remove it, or use --no-verify if you are sure." >&2
      exit 1
    fi
    
    #!/usr/bin/env bash
    # .githooks/commit-msg: require a Conventional Commits subject, type(scope)!: description
    if ! head -1 "$1" | grep -qE '^(feat|fix|docs|chore|refactor|test|ci|perf|build)(\([a-z0-9-]+\))?!?: .+'; then
      echo "commit-msg: start the subject with a type, e.g. 'fix: handle an empty cart'" >&2
      exit 1
    fi
    

    git diff --cached is what's staged, the exact content of the commit; grep '^+' keeps only added lines, so removing a secret is never blocked. The commit-msg hook gets the path of the message file as $1.

    $ chmod +x .githooks/*
    $ git config core.hooksPath .githooks      # use the tracked folder instead of .git/hooks
    $ git commit -m "feat: add config"         # with API_KEY = "sk-live-1234567890" staged
    pre-commit: this commit seems to add a secret. Remove it, or use --no-verify if you are sure.
    $ git commit -m "add config"               # safe code, bad message
    commit-msg: start the subject with a type, e.g. 'fix: handle an empty cart'
    $ git commit -m "feat: read the API key from the environment"
    
    chmod +x .githooks/*
    • chmodchange permissions
    git config core.hooksPath .githooks
    • gitversion control
    • configread or set settings
    git commit -m "feat: add config"
    • commitsave staged changes

    .git/hooks is never pushed or cloned, so a tracked .githooks/ folder plus core.hooksPath is how you share hooks. Each teammate runs the git config line once (put it in your README or setup script).

    The pre-commit framework

    Hand-written hooks get hard to share across languages and machines. The pre-commit framework installs hooks from a config file, pins their versions, and runs them only on the files you changed:

    # .pre-commit-config.yaml
    repos:
      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v6.0.0
        hooks:
          - id: trailing-whitespace
          - id: end-of-file-fixer
          - id: check-yaml
          - id: check-added-large-files
          - id: detect-private-key
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.30.1
        hooks:
          - id: gitleaks
    
    $ pipx install pre-commit            # or pip install pre-commit
    $ pre-commit install                 # writes .git/hooks/pre-commit for this clone
    $ pre-commit run --all-files         # check the whole repository once
    $ pre-commit autoupdate              # move every hook to its latest release
    

    JavaScript projects often use husky and lint-staged for the same job. Whichever you pick, run the same tools in CI.

    Hooks are a convenience, not a guarantee

    Anyone can skip them with git commit --no-verify, and a fresh clone has none until someone installs them. Use hooks for fast feedback, and make the same checks required status checks in CI, which no one can skip.

    Scripting GitHub with gh

    The GitHub CLI turns everything you click on GitHub into commands you can script:

    $ gh pr list --author @me --state open        # my open pull requests
    $ gh pr checks                                # CI status of this branch's pull request
    $ gh run list --limit 5                       # recent Actions runs
    $ gh run watch                                # follow a run live
    $ gh run view --log-failed                    # only the logs of failed steps
    $ gh issue list --label bug --json number,title --jq '.[] | "\(.number) \(.title)"'
    $ gh secret set DEPLOY_KEY < key.txt          # set an Actions secret from a file
    $ gh api repos/{owner}/{repo}/branches/main/protection   # any REST endpoint gh doesn't wrap
    
    gh secret set DEPLOY_KEY < key.txt
    • <read input from a file

    --json and --jq give you stable output for scripts. gh api reaches the whole GitHub API with your login, and {owner}/{repo} fill in from the current repository.

    Your first GitHub Actions workflow

    GitHub Actions runs jobs on GitHub's machines when something happens in your repository. A workflow is a YAML file in .github/workflows/:

    # .github/workflows/ci.yml
    name: CI
    on:
      pull_request:
      push:
        branches: [main]
    jobs:
      test:
        runs-on: ubuntu-latest
        timeout-minutes: 10
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-python@v5
            with:
              python-version: '3.12'
          - run: pip install pre-commit pytest
          - run: pre-commit run --all-files
          - run: pytest
    
    PartMeans
    onwhich events start it: here every pull request, and pushes to main
    jobs.test.runs-ona fresh Ubuntu machine for each run
    usesa reusable action: check out the code, install Python
    runa shell command; a non-zero exit fails the job

    Push it on a branch and open a pull request: the run appears under the pull request's checks. Then, in your main ruleset, make test a required status check, and nothing failing can merge. The CI/CD course goes much deeper: caching, matrices, environments, secrets and deployments.

    In production

    Pin third-party actions to a full commit SHA (uses: owner/action@<sha> # v1.2.3), not a moving tag: a compromised tag would run someone else's code with your secrets. Dependabot can keep the pins up to date (the Security lesson).

    ⌘
    Mini mission

    Guard a repository at both ends

    Add a shared pre-commit hook that blocks secrets and a commit-msg hook that requires Conventional Commits, prove each one rejects a bad commit and accepts a good one, then add a CI workflow that runs the same checks on every pull request.

    Input: Any repository (the shop repository works)Output: .githooks/ in the repository, and .github/workflows/ci.yml running on pull requests
    Reveal one safe solution
    $ mkdir -p .githooks
    $ nano .githooks/pre-commit         # paste the pre-commit hook from this lesson
    $ nano .githooks/commit-msg         # paste the commit-msg hook
    $ chmod +x .githooks/* && git config core.hooksPath .githooks
    $ echo 'API_KEY = "sk-live-1234567890"' > config.py && git add config.py
    $ git commit -m "feat: add config"
    pre-commit: this commit seems to add a secret. Remove it, or use --no-verify if you are sure.
    $ printf 'import os\nAPI_KEY = os.environ["API_KEY"]\n' > config.py && git add config.py
    $ git commit -m "add config"
    commit-msg: start the subject with a type, e.g. 'fix: handle an empty cart'
    $ git commit -m "feat: read the API key from the environment"
    $ git add .githooks && git commit -m "chore: share the Git hooks"
    $ mkdir -p .github/workflows && nano .github/workflows/ci.yml   # the workflow from this lesson
    $ git switch -c ci/add-workflow && git add .github && git commit -m "ci: run checks on pull requests"
    $ git push -u origin ci/add-workflow && gh pr create --fill
    $ gh pr checks --watch
    
    mkdir -p .githooks
    • mkdirmake a directory
    • -pcreate missing parents, no error if it exists
    chmod +x .githooks/* && git config core.hooksPath .githooks
    • chmodchange permissions
    • &&run the next command only if this one worked
    • gitversion control
    • configread or set settings
    echo 'API_KEY = "sk-live-1234567890"' > config.py && git add config.py
    • echoprint text
    • >write output to a file, replacing it
    • addstage changes
    git commit -m "feat: add config"
    • commitsave staged changes
    git push -u origin ci/add-workflow && gh pr create --fill
    • pushupload commits

    The two rejected commits never happened; git log shows only the good ones. The workflow's run on the pull request proves the checks also run where nobody can skip them. If the repository has no tests yet, remove the pytest step: pytest exits with code 5 when it finds nothing to run, which fails the job.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    When does a pre-commit hook run, and what happens if it exits with 1?

    Go deeper

    Official reading for this lesson

    • GuidePro Git: Git hooksPro Git ↗
    • Docsgithooks documentationGit ↗
    • Toolpre-commit frameworkpre-commit ↗
    • Toolgitleaks: find secrets in Git repositoriesgitleaks ↗
    • DocsGitHub CLI manualGitHub ↗
    • DocsGitHub Docs: quickstart for GitHub ActionsGitHub Docs ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousTags and releases: version numbers people can trustNext lesson →Security and extras: secrets, Dependabot, LFS, submodules and Pages