Before we dive into GitHub Actions itself, you need to understand what problem it is solving. Every software team faces the same painful cycle — a developer writes code, manually runs tests, manually builds the app, manually copies files to a server, and hopes nothing breaks. This works when you have one developer and one server. It falls apart completely when you have a team of ten developers pushing code five times a day. **CI/CD is the solution to this chaos.** CI stands for Continuous Integration — every time someone pushes code, automated tests run immediately to catch problems before they reach anyone else. CD stands for Continuous Delivery or Deployment — after tests pass, the code is automatically packaged and shipped to a server without anyone doing it manually. The result is that instead of a stressful Friday afternoon deployment where everyone holds their breath, code goes out dozens of times a day with confidence. Bugs are caught within minutes of being introduced, not days later when they have caused real damage.  GitHub Actions is GitHub's built-in CI/CD platform. You do not need to set up a separate Jenkins server, pay for CircleCI, or learn a different tool. Everything is configured in YAML files that live directly in your repository alongside your code. When something happens in your repo — a push, a pull request, a tag — GitHub spins up a fresh virtual machine, runs your instructions, and reports back. Here is what a typical automated pipeline looks like: ``` Developer pushes code to GitHub ↓ GitHub Actions detects the push ↓ Spins up a fresh Ubuntu virtual machine ↓ Checks out your code ↓ Installs dependencies ↓ Runs linter (code style check) ↓ Runs unit tests ↓ Builds the application ↓ Deploys to server (if on main branch) ↓ Sends success/failure notification ``` All of that happens automatically, every single time, in the same order, with no human intervention. That is the power of CI/CD. #### How GitHub Actions Compares to Alternatives | Tool | Where It Runs | Setup Required | Free Tier | Config Format | |---|---|---|---|---| | GitHub Actions | GitHub's servers | Zero — built in | 2,000 min/month | YAML in repo | | Jenkins | Your own server | High — self-hosted | Free but costly to host | Groovy DSL | | CircleCI | CircleCI's servers | Low | 6,000 min/month | YAML | | GitLab CI | GitLab's servers | Low | 400 min/month | YAML | | Bitbucket Pipelines | Atlassian's servers | Low | 50 min/month | YAML | For most teams using GitHub, GitHub Actions is the obvious first choice — it is already there, it is deeply integrated, and the free tier covers the vast majority of small to medium projects. ---
Understanding GitHub Actions means understanding five concepts. Everything else is just combinations of these five. Skip this section and the rest will not make sense. Learn it properly and everything clicks. The five concepts build on each other like layers: ``` Workflow (the whole automation — one YAML file) ↓ Event (what triggers it to run) ↓ Job (a group of steps that run together on one machine) ↓ Step (one single task inside a job) ↓ Action (a reusable pre-built step from the marketplace) ``` Think of a **Workflow** like a recipe book. An **Event** is someone deciding to cook. A **Job** is one chef working in the kitchen. **Steps** are the individual instructions in the recipe. **Actions** are pre-made ingredients you buy instead of making from scratch.  #### Workflow A workflow is a YAML file stored in `.github/workflows/` inside your repository. One repo can have many workflow files — one for CI testing, one for deployment, one for automated releases. Each file is completely independent. ```yaml # .github/workflows/my-workflow.yml # This file defines one complete automation name: My First Workflow # shown in the GitHub UI on: push # runs whenever code is pushed jobs: my-job: runs-on: ubuntu-latest steps: - run: echo "Hello!" ``` #### Event An event is what causes a workflow to run. GitHub watches your repository for activity and fires the appropriate workflows when matching events occur. ```yaml on: push # any push to any branch on: pull_request # any pull request opened on: [push, pull_request] # either of these events on: push: branches: [main] # only pushes to main branch on: schedule: - cron: '0 9 * * 1' # every Monday at 9am on: workflow_dispatch: # manual button in GitHub UI ``` #### Job A job is a set of steps that run together on the same machine. By default, multiple jobs in the same workflow run in parallel — at the same time. You can make jobs depend on each other using `needs`. Each job gets a fresh, clean virtual machine. Nothing persists between jobs unless you explicitly share it via artifacts. #### Step A step is one single task inside a job. Steps run one after another, in order. If a step fails, by default the job stops and the remaining steps are skipped. A step is either a `run` command (a shell command you write) or a `uses` action (a pre-built action from the marketplace). #### Action An action is a reusable, pre-built piece of automation that someone has packaged and shared. Instead of writing 30 lines of shell script to set up Node.js, you use `actions/setup-node@v4` — one line that handles everything correctly across all operating systems. The GitHub Marketplace has over 20,000 actions. The most commonly used ones are maintained by GitHub itself and are completely free. ---
The best way to learn GitHub Actions is to write one and watch it run. Let us build a real workflow from scratch, understand every line, and then make it do something useful. Before writing anything, create the workflows directory. GitHub Actions will not find your files unless they are in exactly this location: ```bash mkdir -p .github/workflows ``` The `-p` flag creates parent directories if they do not exist. Now create your first workflow file: ```yaml # .github/workflows/hello.yml name: Hello World # name shown in GitHub UI under Actions tab on: # what triggers this workflow push: branches: [main] # only run when pushing to main pull_request: branches: [main] # also run on pull requests targeting main jobs: say-hello: # job ID — you name this, can be anything runs-on: ubuntu-latest # use GitHub's Ubuntu virtual machine steps: - name: Checkout code # step 1 — download your repo onto the runner uses: actions/checkout@v4 # official GitHub action — always use this first - name: Print a message # step 2 — run a shell command run: echo "Hello from GitHub Actions!" - name: Show some info # step 3 — multiple commands with | run: | echo "Current directory: $(pwd)" echo "Files here:" ls -la echo "Node version: $(node --version)" ``` Now push this file to GitHub: ```bash git add .github/workflows/hello.yml git commit -m "Add Hello World workflow" git push origin main ``` Go to your repository on GitHub, click the **Actions** tab, and you will see your workflow running. Click on it to see each step's output in real time. This is where you will spend a lot of time debugging — the logs show exactly what ran and what failed. #### Reading the Workflow Run When you open a workflow run in GitHub, you see: ``` Workflow Run ↓ say-hello (job) ↓ Set up job (automatic — GitHub prepares the machine) Checkout code (your step 1) Print a message (your step 2) Show some info (your step 3) Complete job (automatic — cleanup) ``` Green checkmarks mean success. Red X means failure. Click any step to expand and see the full output. The step that turned red is where your problem is. ---
A trigger (called an `on` event) is what causes your workflow to start. Choosing the right trigger is important — trigger too broadly and you waste compute minutes on unnecessary runs. Trigger too narrowly and automation does not kick in when you need it. Understanding triggers properly means you can build smart pipelines: run tests on every push, but only deploy on pushes to main, and run cleanup jobs every night at midnight.  #### Push and Pull Request — The Most Common Triggers ```yaml on: push: branches: [main, develop] # only these branches trigger the workflow paths: # only trigger if these paths changed - 'src/**' # any file inside src/ - 'package.json' # or package.json specifically tags: - 'v*' # trigger on version tags like v1.0.0, v2.3.1 pull_request: branches: [main] # PRs targeting main types: [opened, synchronize] # when PR is opened or new commits pushed to it ``` The `paths` filter is extremely useful — if you have a monorepo with frontend and backend, you can trigger the frontend pipeline only when frontend files change. #### Schedule — Time-Based Triggers ```yaml on: schedule: - cron: '0 9 * * 1' # Monday at 9am UTC - cron: '0 0 * * *' # every day at midnight - cron: '*/15 * * * *' # every 15 minutes ``` Cron format is five fields: `minute hour day-of-month month day-of-week`. Use [crontab.guru](https://crontab.guru) to build and verify cron expressions without memorising the syntax. Common uses for scheduled triggers: nightly database backups, weekly dependency update checks, daily health check reports. #### Manual Trigger — Run From the GitHub UI ```yaml on: workflow_dispatch: # adds a "Run workflow" button in GitHub UI inputs: environment: description: 'Which environment to deploy to' required: true default: 'staging' type: choice options: [staging, production] run_tests: description: 'Run tests before deploying' type: boolean default: true ``` With `workflow_dispatch`, you get a button in the Actions tab that lets you manually trigger the workflow and fill in parameters. This is perfect for one-off deployment runs or maintenance tasks. ---
A job is where the actual work happens. Each job runs on its own fresh virtual machine called a runner. Understanding how jobs work — how they run in parallel, how to make them depend on each other, and how to share data between them — is what lets you build efficient pipelines. The key mental model: every job is completely isolated. When job A finishes, everything it created (files, installed packages, environment variables) is gone. Job B starts from a completely clean machine. This is intentional — it makes pipelines predictable and reproducible. #### Runner Types GitHub provides free hosted runners. You pick which one to use with `runs-on`: ```yaml jobs: my-job: runs-on: ubuntu-latest # Ubuntu (most common, fastest, cheapest) windows-test: runs-on: windows-latest # Windows Server (10x more expensive per minute) mac-test: runs-on: macos-latest # macOS (10x more expensive per minute) ``` Use `ubuntu-latest` for everything unless you specifically need to test on Windows or macOS. It is faster, has more pre-installed tools, and costs the same as the free tier gives you more minutes for. #### Parallel Jobs — Run Multiple Things at Once By default, jobs in the same workflow run in parallel. This is one of the biggest time-savers in GitHub Actions: ```yaml jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test # runs at the same time as the jobs below lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm run lint # runs in parallel with unit-tests type-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm run type-check # also runs in parallel ``` All three jobs start simultaneously. If your tests take 3 minutes, your lint takes 1 minute, and your type-check takes 2 minutes — the whole pipeline finishes in 3 minutes, not 6.
Every real pipeline needs configuration values — API keys, database URLs, deployment targets, feature flags. GitHub Actions has a clear system for handling these, and understanding the difference between variables and secrets is essential for both security and functionality. The core rule is simple: **anything sensitive goes in Secrets, anything non-sensitive goes in Variables**. Secrets are encrypted and never shown in logs. Variables are plain text and visible in logs. #### Setting Variables in Workflows Variables can be set at three levels — workflow level (available to all jobs), job level (available to all steps in that job), or step level (available only to that specific step): ```yaml env: APP_NAME: myapp # workflow level — all jobs can use this NODE_ENV: production jobs: deploy: runs-on: ubuntu-latest env: DEPLOY_DIR: /opt/myapp # job level — only this job steps: - name: Build env: BUILD_MODE: production # step level — only this step run: | echo "App: $APP_NAME" echo "Deploy to: $DEPLOY_DIR" echo "Build mode: $BUILD_MODE" ``` GitHub also provides automatic variables you can always use: ```yaml run: | echo "Repository: $GITHUB_REPOSITORY" # org/repo-name echo "Branch: $GITHUB_REF_NAME" # main, develop, etc. echo "Commit SHA: $GITHUB_SHA" # full commit hash echo "Run number: $GITHUB_RUN_NUMBER" # increments each run echo "Actor: $GITHUB_ACTOR" # who triggered the run ``` #### Secrets — Storing Sensitive Values Secrets are stored encrypted in GitHub and injected into workflows at runtime. You can never read a secret's value after storing it — only overwrite it. GitHub automatically redacts secret values from workflow logs even if you accidentally print them. Add secrets via the GitHub UI: **Repository → Settings → Secrets and variables → Actions → New repository secret** Or via GitHub CLI: ```bash gh secret set API_KEY --body "your-secret-value-here" gh secret set DATABASE_URL --body "postgres://user:pass@host:5432/db" gh secret list # shows names only, never values ``` Use secrets in workflows with the `${{ secrets.SECRET_NAME }}` syntax: ```yaml steps: - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 ```
Before we dive into GitHub Actions itself, you need to understand what problem it is solving. Every software team faces ...
Understanding GitHub Actions means understanding five concepts. Everything else is just combinations of these five. Skip...
The best way to learn GitHub Actions is to write one and watch it run. Let us build a real workflow from scratch, unders...
A trigger (called an on event) is what causes your workflow to start. Choosing the right trigger is important — trigger ...
A job is where the actual work happens. Each job runs on its own fresh virtual machine called a runner. Understanding ho...
Every real pipeline needs configuration values — API keys, database URLs, deployment targets, feature flags. GitHub Acti...
Now that you understand the building blocks, let us put them together into pipelines you would actually use in a real pr...
Two features make your pipelines dramatically faster and more useful — caching and artifacts. They are related but solve...
As your organisation grows, you will find yourself copying the same workflow code across dozens of repositories. Reusabl...
These are the things that make the difference between a workflow that kind of works and one that is reliable, fast, and ...
Master this concept and view production exercises.
GitHub Actions is not the end of your CI/CD journey — it is the entry point. Once you are comfortable with workflows, he...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.