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.
| Concern | Git | GitHub |
|---|---|---|
| Role | Distributed version history | Hosted collaboration, governance and automation |
| Key objects | commits, branches, tags | pull requests, Actions, rulesets, environments |
| DevOps value | Auditable source change | CI/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-originGit 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.
| Command | What it does | Production guidance |
|---|---|---|
| status | Shows local state | Run before commits and branch switches |
| fetch | Downloads remote refs only | Safest way to inspect remote work |
| pull | Fetches then integrates | Use `--ff-only` on protected branches |
| merge | Combines histories | Preserves branch topology |
| rebase | Replays commits on a base | Never rebase shared history |
| revert | New commit undoing a commit | Preferred for published history |
| reset | Moves 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-checkCreate 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.comProfessional 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 strategy | Use when | Trade-off |
|---|---|---|
| Squash | A cohesive feature branch | Individual WIP commits disappear |
| Merge commit | Branch context matters | History is less linear |
| Rebase merge | Team understands rewritten commits | Commit 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 → mergeRepository 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.
| Runner | Best fit | Risk |
|---|---|---|
| GitHub-hosted | Standard CI and cloud deploy | Keep permissions minimal |
| Self-hosted | Private networks/special tooling | Untrusted code can compromise it |
| Jenkins | Legacy shared libraries/agents | Controller, 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: errorTriggers, 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.
| Type | Use | Never store |
|---|---|---|
| Variable | region, image name, feature flag | passwords or tokens |
| Secret | token, private key, client secret | public configuration |
| Environment secret | production-only access | untrusted 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 productionTerraform, 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.ymlReusable 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.
| Failure | Check | Fix |
|---|---|---|
| Workflow not triggered | event, branch and path filters | Confirm workflow file exists on triggering branch |
| Secret missing | fork/Environment scope | Keep deployment after merge and use protected environment |
| OIDC denied | `id-token: write`, IAM audience and subject | Scope trust to repository/environment correctly |
| ECR denied | role policy and repository name | Check AWS identity and ECR repo policy |
| kubectl denied | EKS access and RBAC | Run `kubectl auth can-i` and map least privilege |
| Docker failure | plain build output | Run `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