Git, GitHub and the DevOps delivery path

Git is the distributed version-control system that records commits, branches and tags. GitHub is the collaboration platform around Git repositories: it adds pull requests, review, rulesets, security and automation. Git records the change; GitHub coordinates the people and systems that decide whether the change is safe to deploy.

In DevOps, source control is the start of CI/CD. A pull request triggers repeatable checks; a protected merge can trigger artifact publication and an approved deployment. GitHub connects naturally to Actions, Docker, Kubernetes, AWS, Terraform, Ansible, Jenkins and security/monitoring tools.

ConcernGitGitHub
RoleDistributed version historyHosted collaboration, governance and automation
Key objectscommits, branches, tagspull requests, Actions, rulesets, environments
DevOps valueAuditable source changeCI/CD triggers, approval and evidence
flowchart TD
  A[Developer] --> B[Git]
  B --> C[GitHub repository]
  C --> D[Pull Request]
  D --> E[Review and required checks]
  E --> F[GitHub Actions]
  F --> G[Build and test]
  G --> H[Security scan]
  H --> I[Docker image]
  I --> J[Container registry]
  J --> K[Approved deployment]
  K --> L[Kubernetes or AWS]

At each stage: Git captures intent; PRs provide review; Actions create evidence; image tags identify the artifact; environments gate deployment; Kubernetes/AWS operate the release.

Install, configure and inspect Git

Set the author identity once per machine. `git config --list --show-origin` explains where a setting comes from when behaviour is unexpected. Authentication belongs in a credential manager or SSH agent, never in a repository URL or configuration file.

# Ubuntu/Debian
sudo apt update && sudo apt install -y git
# macOS
xcode-select --install
brew install git
# Windows PowerShell
winget install --id Git.Git -e --source winget

git --version
git config --global user.name "Your Name"
git config --global user.email "your-email@example.com"
git config --global init.defaultBranch main
git config --list --show-origin

Git fundamentals: working tree to remote

The working directory is what you edit. The staging area is the proposed next commit. The local repository stores commits. The remote repository is the collaboration point. Review `git diff` before staging and `git diff --staged` before committing; these two checks prevent accidental configuration, credential and generated-file commits.

CommandWhat it doesProduction guidance
statusShows local stateRun before commits and branch switches
fetchDownloads remote refs onlySafest way to inspect remote work
pullFetches then integratesUse `--ff-only` on protected branches
mergeCombines historiesPreserves branch topology
rebaseReplays commits on a baseNever rebase shared history
revertNew commit undoing a commitPreferred for published history
resetMoves branch pointer`--hard` discards changes; use only when certain
git init
git clone git@github.com:<github-username>/<repository-name>.git
git status
git diff
git add src/ .github/workflows/ci.yml
git diff --staged
git commit -m "feat: add health endpoint"
git log --oneline --decorate --graph -20

git branch --show-current
git switch -c feature/health-check
git switch main
git checkout -b feature/legacy-syntax
git fetch origin --prune
git pull --ff-only origin main
git push -u origin feature/health-check
git remote -v
git tag -a v1.2.0 -m "Release v1.2.0"
git stash push -m "wip: investigate timeout"
git restore path/to/file
git restore --staged path/to/file
git revert <published-commit-sha>
git cherry-pick <commit-sha>

# DANGEROUS: discards tracked working changes
git reset --hard <commit-sha>
# Safer force push: refuses to overwrite unseen remote commits
git push --force-with-lease origin feature/health-check

Create a GitHub repository and authenticate with SSH

Create an empty repository in GitHub, then connect the local history. HTTPS normally uses a credential manager or personal access token; SSH keeps the private key on the workstation and is a strong developer default. Add only the public key to GitHub, and protect the private key with a passphrase.

mkdir devops-github-actions-demo
cd devops-github-actions-demo
git init
printf '# DevOps GitHub Actions demo\n' > README.md
printf '.env\nnode_modules/\n.terraform/\n*.tfstate*\n' > .gitignore
git add .
git commit -m "chore: initial project setup"
git remote add origin git@github.com:<github-username>/<repository-name>.git
git branch -M main
git push -u origin main

ssh-keygen -t ed25519 -C "your-email@example.com"
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
ssh -T git@github.com

Professional branching, pull requests and reviews

A practical model uses `main` for production-ready code, `develop` for the next integrated release, short-lived `feature/*` and `bugfix/*` branches, `release/*` to stabilize a release and `hotfix/*` from production. Use `integration/*` only when dependent features need joint testing before they belong in `develop`.

A pull request is the review record: CI validates the branch, reviewers assess change and risk, required checks block unsafe merges, and the merge strategy decides the shape of history. Squash merge is ideal for one cohesive feature, merge commits preserve topology, and rebase merge keeps a linear history but rewrites commit IDs.

Merge strategyUse whenTrade-off
SquashA cohesive feature branchIndividual WIP commits disappear
Merge commitBranch context mattersHistory is less linear
Rebase mergeTeam understands rewritten commitsCommit hashes change
main
├── release/*
├── hotfix/*
└── develop
    ├── integration/*
    │   ├── feature/*
    │   └── feature/*
    ├── feature/*
    └── bugfix/*

git switch develop
git pull --ff-only origin develop
git switch -c feature/user-authentication
git add .
git commit -m "feat: add user authentication"
git push -u origin feature/user-authentication
# Open a PR: feature/user-authentication → develop
# Developer → feature branch → PR → CI → review → approval → merge

Repository security and governance

Use rulesets to require pull requests, minimum approvals, CODEOWNER review, successful checks and resolved conversations. Block direct pushes, force pushes and branch deletion on `main`. Enable secret scanning with push protection, Dependabot, dependency review and code scanning. Signed commits strengthen provenance but do not replace review or least privilege.

GitHub Environments separate development, staging and production. Restrict production to release branches or main, require reviewers, and ensure only the protected deployment job receives deployment credentials.

# .github/CODEOWNERS
*                       @<github-username>/platform-engineering
/.github/workflows/    @<github-username>/platform-engineering
/terraform/             @<github-username>/cloud-infrastructure
/k8s/                    @<github-username>/platform-engineering

# Main ruleset: require PR, 2 approvals, CODEOWNER review, status checks,
# resolved conversations; block force push/deletion; restrict bypass actors.

GitHub Actions: workflows, runners and YAML

A workflow is a YAML file in `.github/workflows/`. Events trigger runs. Workflows contain jobs, jobs run on runners and jobs contain ordered steps. A step invokes an action or executes a command. GitHub-hosted runners are ephemeral and suitable for normal CI; self-hosted runners are appropriate for private networks or special tooling but need patching, isolation and a strict trust boundary.

Artifacts preserve test reports or build output. Caches speed up dependency downloads but should be used cautiously in privileged workflows. Variables hold non-sensitive configuration; secrets hold sensitive values; environments scope deployment approvals and environment-specific values.

RunnerBest fitRisk
GitHub-hostedStandard CI and cloud deployKeep permissions minimal
Self-hostedPrivate networks/special toolingUntrusted code can compromise it
JenkinsLegacy shared libraries/agentsController, plugins and credentials need ownership
.github/
└── workflows/
    ├── ci.yml
    ├── deploy-production.yml
    └── reusable-build.yml

# Jenkins vs GitHub Actions
# Actions is GitHub-native: PR checks, hosted runners, environments and reusable workflows.
# Jenkins is highly flexible with a mature plugin model but needs more platform ownership.

First GitHub Actions CI workflow

This complete workflow runs on pushes and pull requests, checks out code, pins the Node runtime, uses package caching, linting, unit tests and a build. In production, pin third-party actions to reviewed immutable commit SHAs; major tags below are readable for learning.

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
permissions:
  contents: read
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint --if-present
      - run: npm test -- --ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: web-build
          path: dist
          if-no-files-found: error

Triggers, variables, secrets and environments

Use `push` for merged changes, `pull_request` for PR validation, `workflow_dispatch` for an explicit operator run, `schedule` for maintenance, `workflow_call` for reusable workflows and `repository_dispatch` for controlled external events. Branch/path filters reduce unnecessary runs but must not exclude a required check.

Repository variables are for non-sensitive values such as region or image name. Secrets are referenced as `${{ secrets.SECRET_NAME }}` and must never be committed. Prefer OIDC-issued temporary cloud credentials over permanent AWS access keys. Secrets are intentionally not available to untrusted fork pull requests.

TypeUseNever store
Variableregion, image name, feature flagpasswords or tokens
Secrettoken, private key, client secretpublic configuration
Environment secretproduction-only accessuntrusted PR inputs
on:
  push:
    branches: [main, 'release/**']
    paths: ['src/**', 'package.json', 'package-lock.json', '.github/workflows/**']
  pull_request:
    branches: [main]
  workflow_dispatch:
  schedule:
    - cron: '17 3 * * 1'
  workflow_call:

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
# Runtime only; never echo secrets
# API_TOKEN: ${{ secrets.API_TOKEN }}

Docker, AWS OIDC, Amazon ECR and EKS

Build a tested image from a small context, run as a non-root user and publish by commit SHA. A mutable `latest` tag is convenient for development but is not a production deployment reference. GitHub Actions authenticates to AWS through OIDC: AWS trusts the GitHub OIDC provider and a tightly scoped IAM role for a specific repository/branch/environment, then issues temporary credentials.

The production job uses a protected `production` Environment, pushes an immutable ECR tag and rolls out that tag to EKS. The role should have only ECR and deployment permissions required; never use AdministratorAccess or static access keys for this path.

# Dockerfile
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:22-alpine
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build
USER node
EXPOSE 3000
CMD ["npm", "start"]

# .github/workflows/deploy-production.yml
name: Build and deploy production
on:
  push: { branches: [main] }
permissions: { contents: read, id-token: write }
concurrency: { group: production, cancel-in-progress: false }
jobs:
  deploy:
    environment: production
    runs-on: ubuntu-latest
    env:
      AWS_REGION: ${{ vars.AWS_REGION }}
      ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
      EKS_CLUSTER_NAME: ${{ vars.EKS_CLUSTER_NAME }}
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
      - id: ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: Build and push
        env: { REGISTRY: ${{ steps.ecr.outputs.registry }}, IMAGE_TAG: ${{ github.sha }} }
        run: |
          docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
      - name: Deploy and verify
        env: { REGISTRY: ${{ steps.ecr.outputs.registry }}, IMAGE_TAG: ${{ github.sha }} }
        run: |
          aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$AWS_REGION"
          kustomize edit set image app="$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
          kubectl apply -k k8s/overlays/production
          kubectl rollout status deployment/app -n production --timeout=5m
          kubectl get deployments,pods,services -n production

Terraform, Ansible and production environments

For every infrastructure pull request, run format checks, initialization, validation and plan. A plan is a review artifact; `apply` changes shared state and belongs only in an approved protected workflow. Use remote encrypted state with locking. After provisioning, Ansible can configure hosts and deploy services; its inventory and vault policy need the same review discipline as infrastructure code.

A common promotion flow is feature branch → pull request → CI → develop → development → release → staging → approval → main → production. Keep CI and deploy workflows separate so a test job never inherits cloud deployment privileges.

terraform fmt -check -recursive
terraform init -input=false
terraform validate
terraform plan -input=false -out=tfplan
# Apply only in a protected environment after approval
terraform apply -input=false tfplan

ansible --version
ansible-inventory --list -i inventory/production.yml
ansible all -m ping -i inventory/production.yml
ansible-playbook -i inventory/production.yml playbook.yml --check --diff
ansible-playbook -i inventory/production.yml playbook.yml

.github/workflows/
├── ci.yml
├── deploy-dev.yml
├── deploy-staging.yml
├── deploy-production.yml
└── reusable-build.yml

Reusable workflows and production best practices

Large organizations avoid duplicated YAML by using `workflow_call` for reusable build/deploy logic and composite actions for small repeated step sequences. Reusable workflows make it easier to standardize scanning, permissions and OIDC. Use concurrency controls to prevent simultaneous production deployments; retain immutable artifacts and a tested rollback procedure.

# .github/workflows/reusable-build.yml
on:
  workflow_call:
    inputs:
      node-version: { required: true, type: string }
jobs:
  build:
    runs-on: ubuntu-latest
    permissions: { contents: read }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ inputs.node-version }}', cache: npm }
      - run: npm ci
      - run: npm test
      - run: npm run build

# Checklist: pin actions, least privilege, OIDC, no hard-coded secrets, protected
# environments, immutable tags, timeouts, artifacts, scans, reusable workflows,
# production concurrency and isolated self-hosted runners.

Troubleshooting, cheat sheet and interview practice

Start with the run log and the exact failed command. Avoid retrying a deployment before understanding whether it partially changed cloud state. For Terraform locks, verify the active owner before any force unlock. For EKS, validate both IAM/EKS access and Kubernetes RBAC.

FailureCheckFix
Workflow not triggeredevent, branch and path filtersConfirm workflow file exists on triggering branch
Secret missingfork/Environment scopeKeep deployment after merge and use protected environment
OIDC denied`id-token: write`, IAM audience and subjectScope trust to repository/environment correctly
ECR deniedrole policy and repository nameCheck AWS identity and ECR repo policy
kubectl deniedEKS access and RBACRun `kubectl auth can-i` and map least privilege
Docker failureplain build outputRun `docker build --progress=plain .` locally
# Daily Git
git status && git diff && git add <path> && git commit -m "type: message"
git fetch origin --prune && git pull --ff-only origin main
git restore <file> && git revert <sha> && git stash push -m "wip"

# Deployment validation and rollback
aws sts get-caller-identity
aws eks update-kubeconfig --name <eks-cluster-name> --region <aws-region>
kubectl auth can-i get pods -n production
kubectl get events -n production --sort-by=.lastTimestamp
kubectl rollout history deployment/app -n production
kubectl rollout undo deployment/app -n production

# Interview answers
# fetch vs pull: fetch updates refs only; pull fetches then integrates.
# merge vs rebase: merge preserves topology; rebase rewrites commits for linear history.
# Recover deleted commit: use git reflog, make a recovery branch, then cherry-pick/revert.
# Secure Actions to AWS: OIDC + restricted IAM trust policy + least-privilege role.
# Prevent direct production: protected main + required PR checks + Environment approvals.

Final hands-on project

Build this end to end: create a repository, deliver a feature through a pull request, enforce GitHub Actions CI, add dependency and container scanning, build a SHA-tagged image, exchange OIDC for AWS credentials, push to ECR, deploy to an EKS development namespace, validate rollout and health, then promote through protected staging and production environments.

devops-github-actions-demo/
├── .github/workflows/   # CI, deploy and reusable workflow definitions
├── src/                 # application source
├── tests/               # unit and integration tests
├── docker/              # Docker support files
├── k8s/                 # base manifests and environment overlays
├── terraform/           # reviewed infrastructure modules and roots
├── ansible/             # inventories, roles and playbooks
├── scripts/             # deterministic local/CI helpers
├── .gitignore           # excludes secrets, dependencies and state
├── Dockerfile           # reproducible application package
└── README.md            # setup, architecture and runbook
← Back to all articles