| | |

GitHub 17 🌐 GitHub Actions Fundamentals and Workflow Files

⏱️ Estimated read: 9 min | |

GitHub Actions is an automation platform built into GitHub. It runs workflows—a sequence of jobs and steps—in response to events like a push, a pull request, or a scheduled time. A workflow is defined in a YAML file inside .github/workflows/. The file names the triggers, defines the jobs, specifies the runner environment, and lists the steps to execute. The platform is powerful, but its vocabulary is precise: a workflow contains jobs, a job contains steps, a step runs a command or an action, and an action is a reusable unit of code from the marketplace or a local repository. Understanding this hierarchy is the difference between writing a workflow that works and one that fails mysteriously.

Key point: A workflow is a YAML file in .github/workflows/. Jobs run in parallel by default, and each job runs on a fresh virtual machine. Steps within a job run sequentially and share the same filesystem.


Why GitHub Actions exists

The CI/CD integration problem. Before Actions, continuous integration required a separate service—Travis CI, CircleCI, Jenkins—configured with its own tokens, webhooks, and billing. GitHub Actions embeds the runner directly into the repository. The workflow file is versioned alongside the code, the secrets are managed in GitHub’s settings, and the results appear in the pull request interface. There is no third-party service to configure or pay for separately.

The event-driven problem. A workflow is triggered by events: push, pull_request, schedule, workflow_dispatch, release, and dozens more. The trigger is defined at the top of the YAML file. A push to main can trigger a deployment; a pull request can trigger a test suite; a cron schedule can trigger a security scan. The event-driven model means workflows are not just CI—they are automation for the entire repository lifecycle.

The reusable action problem. Every workflow step is either a shell command (run:) or an action (uses:). An action is a reusable package—written in JavaScript or packaged as a Docker container—that performs a specific task. actions/checkout clones the repository. actions/setup-node installs Node.js. actions/cache caches dependencies. The marketplace hosts thousands of actions, and the same action can be used across repositories, eliminating duplicated shell scripts.

The cost problem. Public repositories run GitHub Actions for free. Private repositories receive a monthly quota of minutes (2,000 for Free tier, 3,000 for Pro, more for Team and Enterprise). The runners are hosted by GitHub—ubuntu-latest, windows-latest, macos-latest—with macOS runners costing more minutes per minute of runtime. Self-hosted runners can be attached to avoid the quota entirely.

The trade-off. GitHub Actions is not the fastest CI platform. The cold start time for a workflow is typically 10–30 seconds, and the runners are shared with other users. For projects that need sub-minute CI times, a self-hosted runner or a dedicated CI service may be preferable. The trade-off is convenience and integration versus raw speed.


a. The workflow file structure

A workflow file is a YAML document with a name, one or more triggers (on:), and one or more jobs (jobs:). The name is optional but appears in the Actions tab. The trigger determines when the workflow runs.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

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

The on: block defines the events. push triggers on every push to main. pull_request triggers on every pull request targeting main. workflow_dispatch enables manual triggering from the Actions tab or the API.

The jobs: block defines the jobs. Each job has a name (test), a runner (ubuntu-latest), and a list of steps. Steps run sequentially. If a step fails, the job stops (unless continue-on-error: true is set).


b. Runners, jobs, and parallel execution

Each job runs on a separate runner—a fresh virtual machine or container. Jobs are independent unless dependencies are declared with needs:. This means jobs run in parallel by default, which reduces total workflow time but requires explicit ordering when one job depends on another.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building"

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing after build"

  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying after both"

The needs: keyword declares dependencies. The test job waits for build to complete. The deploy job waits for both. Without needs:, all three jobs run in parallel.

Available runner labels include ubuntu-latest, ubuntu-24.04, ubuntu-22.04, windows-latest, windows-2022, macos-latest, macos-14, and macos-13. The -latest label is a moving target—it points to the most recent stable image. Pinning a specific version is recommended for reproducibility.


c. Steps, actions, and expressions

A step is a single unit of work. It is either a shell command (run:) or a reusable action (uses:). The run: step executes commands in the runner’s default shell. The uses: step downloads and executes an action from the marketplace or a local path.

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Install dependencies
    run: npm ci

  - name: Run tests
    run: npm test

  - name: Upload coverage
    uses: actions/upload-artifact@v4
    with:
      name: coverage
      path: coverage/

Expressions provide conditional logic and data access. ${{ github.ref }} is the branch or tag reference. ${{ runner.os }} is the operating system. ${{ secrets.MY_TOKEN }} is a secret stored in the repository settings. ${{ matrix.node }} is a value from a matrix strategy.

  - name: Deploy to production
    if: github.ref == 'refs/heads/main'
    run: ./deploy.sh

The if: condition controls whether a step runs. Without it, every step runs. With it, the step only runs when the expression evaluates to true.


Complete Example Session

# ============================================
# PART 1: CREATE THE WORKFLOW DIRECTORY
# ============================================

mkdir -p .github/workflows
# ============================================
# PART 2: CREATE A BASIC WORKFLOW
# ============================================

cat > .github/workflows/ci.yml <<'EOF'
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
EOF
# ============================================
# PART 3: COMMIT AND PUSH
# ============================================

git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push origin main
# ============================================
# PART 4: VIEW THE RUN
# ============================================

# Navigate to the repository → Actions tab.
# The "CI" workflow appears with the latest run.
# ============================================
# PART 5: ADD A MATRIX STRATEGY
# ============================================

# Edit ci.yml to add a matrix:
# strategy:
#   matrix:
#     node: [18, 20, 22]
# Then use ${{ matrix.node }} in setup-node.
# ============================================
# PART 6: ADD CONDITIONAL STEP
# ============================================

# Add to steps:
# - name: Deploy
#   if: github.ref == 'refs/heads/main'
#   run: ./deploy.sh
# ============================================
# PART 7: ADD A SECOND JOB WITH needs
# ============================================

# Add a "deploy" job:
# deploy:
#   needs: test
#   runs-on: ubuntu-latest
# ============================================
# PART 8: TRIGGER MANUALLY
# ============================================

# Add "workflow_dispatch:" to the on: block.
# Then click "Run workflow" in the Actions tab.
# ============================================
# PART 9: VIEW LOGS
# ============================================

# Click on the run → click on the job → expand steps.
# Each step shows its output.

The nine parts trace the workflow lifecycle: directory creation, YAML authoring, commit, viewing the run, adding a matrix, adding a conditional step, adding a dependent job, manual triggering, and inspecting logs.


Quick Reference

Workflow Structure

KeyPurpose
nameWorkflow name (display)
onTrigger events
jobsJobs to run
runs-onRunner label
stepsSteps within a job
usesAction reference
runShell command
withAction inputs
needsJob dependency

Common Triggers

TriggerEvent
pushPush to a branch
pull_requestPull request opened or updated
scheduleCron schedule
workflow_dispatchManual trigger
releaseRelease published

Common Runner Labels

LabelOS
ubuntu-latestUbuntu 24.04
ubuntu-22.04Ubuntu 22.04
windows-latestWindows 2022
macos-latestmacOS 14

Best Practices

✅ Do This:

uses: actions/checkout@v4
<!-- Pin action versions -->                         # ✅
needs: build
<!-- Declare dependencies between jobs -->            # ✅
if: github.ref == 'refs/heads/main'
<!-- Gate deployment steps by branch -->             # ✅

❌ Don’t Do This:

# Use latest tag for actions
<!-- Versions may change unexpectedly -->            # ❌
# Hardcode secrets in the YAML
<!-- Use GitHub secrets instead -->                  # ❌
# Run all jobs without needs
<!-- Parallel jobs may race on shared state -->       # ❌

Common Pitfalls

PitfallWhy It HappensFix
Workflow not runningFile outside .github/workflows/Move to correct directory
actions/checkout failsMissing uses stepAdd actions/checkout@v4
Job runs before dependencyNo needs: specifiedAdd needs: to dependent job
Secrets not accessibleSecrets not defined in repoAdd in Settings → Secrets
Action fails on versionUsing @latestPin specific version

Real-World Examples

1. Basic CI

on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

2. Matrix Strategy

strategy:
  matrix:
    node: [18, 20, 22]

3. Conditional Step

if: github.ref == 'refs/heads/main'

4. Job Dependency

needs: [build]

5. Manual Trigger

on:
  workflow_dispatch:

6. Scheduled Run

on:
  schedule:
    - cron: '0 0 * * *'

7. Secret Access

env:
  TOKEN: ${{ secrets.MY_TOKEN }}

8. Artifact Upload

uses: actions/upload-artifact@v4
with:
  path: dist/

9. Cache Dependencies

uses: actions/cache@v4

10. Multiple Runners

runs-on: ${{ matrix.os }}

Visual

Workflow Hierarchy

┌──────────────────────────────────────────────┐
│  Workflow (ci.yml)                           │
│  ┌─────────────────────────────────────────┐ │
│  │  on: push, pull_request                 │ │
│  └─────────────────────────────────────────┘ │
│                                              │
│  ┌─────────────────────────────────────────┐ │
│  │  Job: test (runs-on: ubuntu-latest)     │ │
│  │  ┌───────────────────────────────────┐  │ │
│  │  │  Step 1: checkout                 │  │ │
│  │  │  Step 2: npm ci                   │  │ │
│  │  │  Step 3: npm test                 │  │ │
│  │  └───────────────────────────────────┘  │ │
│  └─────────────────────────────────────────┘ │
│                                              │
│  ┌─────────────────────────────────────────┐ │
│  │  Job: deploy (needs: test)              │ │
│  │  └── Step: ./deploy.sh                  │ │
│  └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘

Job Execution Flow

push to main
        │
        ▼
┌──────────────┐
│  Job: build  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Job: test   │ (needs: build)
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Job: deploy │ (needs: test)
└──────────────┘

Jobs without needs run in parallel. With needs, they run in sequence.


Summary

ItemValue
Workflow directory.github/workflows/
File extension.yml or .yaml
Trigger keyon
Job keyjobs
Runner keyruns-on
Step typesuses (action) or run (shell)
Job dependencyneeds
Secret access${{ secrets.NAME }}
Manual triggerworkflow_dispatch

Key takeaways:

  • A workflow is YAML in .github/workflows/. The on block defines triggers, the jobs block defines what runs, and the steps block defines the commands.
  • Jobs run in parallel by default. The needs keyword creates a sequential dependency. Without it, jobs race.
  • Steps run sequentially within a job. If a step fails, the job stops unless continue-on-error is set.
  • Actions are reusable packages. uses references an action from the marketplace or a local path. run executes a shell command.
  • Expressions and conditions control behavior. if: gates a step, and ${{ ... }} injects values from the context, secrets, or matrix.
  • Pinning action versions is essential. @v4 is stable; @latest is a moving target that can break unexpectedly.

Remember: A workflow is a script for the repository’s lifecycle. The trigger starts it, the jobs define the work, the steps execute the commands, and the actions provide the reusable pieces. The YAML is declarative: it describes what should happen, not how to do it. GitHub Actions handles the provisioning, the execution, and the reporting. The file is the only artifact.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!