Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Git & GitHub›GitHub Actions — CI/CD Basics

GitHub

GitHub Actions — CI/CD Basics

GitHub Actions is an automation platform built directly into your repository that triggers workflows based on specific events. It matters because it enforces consistency and quality by automating repetitive tasks like testing and deployment, removing the risk of human error in manual processes. You reach for it whenever you want to guarantee that code changes are verified automatically before they ever reach your production environment.

The Anatomy of a Workflow

A GitHub Action workflow is defined in a YAML file located within the .github/workflows directory of your repository. At its core, the system operates on a simple event-driven model: an event triggers the runner, which executes a series of defined jobs. Every job runs in a clean, isolated virtual environment, ensuring that the state of one execution does not bleed into another. By defining 'on', we specify the hooks, such as a push to the main branch or a pull request, that initiate the sequence. The 'jobs' section acts as a container for individual tasks, which run in parallel by default to optimize speed. Understanding this architecture is crucial because it decouples your infrastructure from your development environment. When you define a workflow, you are telling the remote server exactly how to recreate your local project's environment from scratch, ensuring that if it passes in the cloud, the configuration is truly reproducible and reliable.

# .github/workflows/verify.yml
name: Verification Pipeline
on: [push] # Triggers on any commit
jobs:
  test-suite:
    runs-on: ubuntu-latest # Isolated runner environment
    steps:
      - uses: actions/checkout@v4 # Pulls repository code
      - name: Run script
        run: ./test_script.sh # Execute local logic

Understanding Runners and Environments

Runners are the virtual machines provided by GitHub that execute your workflow code. When you specify 'runs-on', you are requesting a fresh compute resource that is provisioned just for your job. This is fundamentally important because it forces you to write code that explicitly handles dependency installation and environment setup, rather than relying on hidden state or local configuration files that exist on your laptop. Because each runner is discarded after the job finishes, you must treat the environment as ephemeral. If your application requires specific permissions or secret keys to interact with external services, you inject those through the 'env' context or 'secrets' store, which keeps your configuration secure and decoupled from your source code. This approach ensures that your pipeline remains deterministic; the same code, run on the same virtual machine image, will always produce the identical output regardless of what is happening on your personal development workstation.

# .github/workflows/environment.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DB_URL }} # Secure injection
    steps:
      - run: echo "Using secret to configure environment"

Defining Jobs and Parallelism

A workflow can contain multiple jobs that either run sequentially or concurrently. By default, jobs run in parallel, which is the most efficient way to utilize resources when tasks are independent, such as running a linter and a security audit simultaneously. However, you can explicitly enforce dependencies using the 'needs' keyword. This creates a Directed Acyclic Graph (DAG) of execution, where a deployment job might wait for the test job to complete successfully before starting. This relationship is vital for maintaining pipeline integrity; you never want to deploy a build that has not passed verification. By structuring your jobs into a logical sequence, you gain granular control over the failure points of your pipeline. If one job fails, the entire workflow can be halted or marked as failing, preventing flawed code from proceeding further. Reasoning about job dependencies is the primary tool for managing complexity in enterprise-grade automation projects.

# .github/workflows/pipeline.yml
jobs:
  lint: # Runs first
    runs-on: ubuntu-latest
    steps: [run: ./lint.sh]
  deploy: # Depends on lint success
    needs: lint
    runs-on: ubuntu-latest
    steps: [run: ./deploy.sh]

Leveraging Reusable Actions

Actions are the building blocks of workflows, serving as reusable units of code that perform specific tasks like checking out the repository, setting up software versions, or uploading build artifacts. Instead of writing custom logic for every repetitive task, you can utilize community-built or official actions. This is powerful because it allows you to stand on the shoulders of the community while maintaining full transparency; since these actions are typically just repositories of code, you can inspect their source, pin them to specific versions, or even fork them to suit your unique requirements. When you use a 'uses' statement, you are essentially importing modular logic into your workflow. This composition pattern allows you to write 'clean' YAML files that focus on the high-level orchestration of your pipeline, delegating low-level operations to specialized, version-controlled actions. Always pin your actions to a specific commit hash to ensure your pipeline does not break due to upstream changes.

# .github/workflows/reusable.yml
steps:
  - uses: actions/checkout@v4 # Versioned, stable logic
  - name: Setup tool
    uses: actions/setup-node@v4
    with:
      node-version: '20' # Consistent environment

Conditional Logic and Filtering

Advanced pipelines often require conditional execution to avoid unnecessary work or to handle complex branching strategies. Using the 'if' keyword, you can prevent a job or step from running unless specific criteria are met—such as only running a production deployment if the branch is exactly 'main'. This is the key to creating smart workflows that understand the difference between a temporary pull request test and a formal release cycle. By combining branch filtering with the 'if' condition, you ensure that your compute resources are consumed only when meaningful work needs to be performed. This level of control allows you to keep your feedback loop fast; developers get instant verification on small features without triggering the heavy, resource-intensive processes reserved for production. Mastering these conditional triggers turns your workflow from a simple sequence of scripts into a robust, responsive system that manages the entire lifecycle of your project's code contributions.

# .github/workflows/conditional.yml
steps:
  - name: Production deploy
    if: github.ref == 'refs/heads/main'
    run: ./deploy_prod.sh # Runs only on main

Key points

  • Workflows are YAML files that define automation triggers and jobs.
  • Events like pushes or pull requests initiate the automated workflow execution.
  • Runners provide clean, isolated environments that prevent configuration drift.
  • The 'needs' keyword creates dependencies between jobs to ensure proper execution order.
  • Actions are modular building blocks that simplify complex pipeline configuration tasks.
  • Always pin your action versions to specific tags or commits for reliability.
  • Conditional logic allows pipelines to behave differently based on the branch or environment.
  • Parallel job execution significantly improves performance for independent build tasks.

Common mistakes

  • Mistake: Committing secrets directly into the repository. Why it's wrong: Anyone with access to the repository can see sensitive credentials in plain text. Fix: Use GitHub Secrets stored in the repository settings and reference them using the ${{ secrets.SECRET_NAME }} syntax.
  • Mistake: Forgetting to set up a checkout step in the workflow. Why it's wrong: The runner environment starts in an empty directory, so your Git repository files are not present by default. Fix: Always include the 'actions/checkout' step as the first job step.
  • Mistake: Triggering workflows on every single push to every branch. Why it's wrong: This wastes CI minutes and clutters the repository history with unnecessary check runs. Fix: Use the 'on: push: branches: [main]' filter to restrict workflows to specific branches.
  • Mistake: Hardcoding environment variables in the YAML workflow file. Why it's wrong: This makes the pipeline rigid and prevents reusability across different branches or deployments. Fix: Define variables within the 'env' section of the workflow or job level to ensure consistency.
  • Mistake: Misunderstanding the difference between the 'run' and 'uses' keywords. Why it's wrong: Using 'run' to execute a shell command instead of calling a reusable action can lead to platform-specific errors. Fix: Use 'uses' for established GitHub Actions and 'run' only for custom shell commands.

Interview questions

What is the fundamental purpose of GitHub Actions in a development workflow?

GitHub Actions is an automation platform integrated directly into your repositories that allows you to create custom workflows for your software development lifecycle. Its primary purpose is to automate, customize, and execute your CI/CD pipelines right where your code lives. By defining a workflow file in the .github/workflows directory, you can ensure that every time you push code or open a pull request, specific tasks—like running tests, linting code, or deploying to an environment—are triggered automatically. This eliminates the manual overhead of managing build servers and ensures that your repository remains in a stable, tested state at all times.

Can you explain the structure of a GitHub Actions workflow file?

A GitHub Actions workflow is defined using YAML syntax and resides in the .github/workflows directory. At its core, the structure starts with the 'name' of the workflow, followed by the 'on' keyword, which defines the events that trigger the pipeline, such as 'push' or 'pull_request'. The 'jobs' key then houses one or more jobs, each containing a 'runs-on' key to specify the virtual machine runner, such as 'ubuntu-latest'. Within each job, you define a sequence of 'steps'. Each step can either run a command using the 'run' keyword or execute a pre-built action using the 'uses' keyword to perform tasks like checking out the repository code.

How do you manage sensitive information like API keys or secrets within a GitHub Actions workflow?

You should never hardcode sensitive credentials directly into your YAML files. Instead, you use GitHub Secrets. You navigate to the repository settings, select 'Secrets and variables,' and add your key-value pairs there. In your workflow file, you access these secrets using the '${{ secrets.SECRET_NAME }}' syntax. For example, to provide a deployment key, you would map it to an environment variable within the step: 'env: API_KEY: ${{ secrets.MY_API_KEY }}'. This ensures that the sensitive value is masked in the logs and is only available to the designated runners during the execution of the workflow.

Compare using a reusable 'Action' versus writing a shell script within a 'run' block in a workflow.

Using a pre-built Action is generally preferred for common tasks, such as checking out code with 'actions/checkout' or setting up an environment, because these actions are community-vetted, optimized for performance, and maintainable. They abstract away complexity into a modular unit. Conversely, writing a shell script within a 'run' block is best suited for project-specific custom logic that doesn't require high-level abstraction. While shell scripts offer total control and are easy to read for quick tasks, they are harder to version control and reuse across different repositories compared to versioned Actions, which can be pinned to specific commits or tags.

What is the difference between an 'Event' trigger and a 'Scheduled' trigger in GitHub Actions?

An 'Event' trigger is reactive; it starts a workflow based on activities occurring within the Git repository, such as a 'push', a 'pull_request', or a 'workflow_dispatch' for manual execution. This is essential for CI/CD because it ensures that code changes are verified immediately upon submission. A 'Scheduled' trigger, defined using the POSIX cron syntax under the 'schedule' key, runs the workflow at specific intervals regardless of code activity. This is useful for tasks that need to run periodically, such as performing daily dependency updates, running deep security scans, or generating automated health reports to ensure the repository remains healthy even when no commits are being made.

Explain how you would implement a multi-stage CI/CD pipeline using 'needs' and 'jobs' in GitHub Actions.

In GitHub Actions, you can orchestrate complex pipelines by defining multiple jobs and using the 'needs' keyword to establish dependencies. For example, you might define a 'test' job that executes your unit tests and a 'deploy' job that handles staging. By adding 'needs: test' to the deploy job, GitHub ensures that the deploy job will not start unless the test job completes successfully. This approach is powerful because it allows you to parallelize independent tasks to save time while enforcing a strict order of operations for critical deployment steps, ensuring that broken code is never pushed to production.

All Git & GitHub interview questions →

Check yourself

1. When defining a workflow, why is it standard practice to place the 'actions/checkout' step before any build or test steps?

  • A.To verify that the runner has an active internet connection to GitHub.
  • B.To fetch the repository's source code so the runner can interact with the files.
  • C.To automatically authenticate the user against the GitHub API.
  • D.To set the working directory to the root of the project by default.
Show answer

B. To fetch the repository's source code so the runner can interact with the files.
The correct answer is fetching the source code because runners start in an empty environment. Option 0 is wrong because runners don't need to check connection state. Option 2 is wrong because the default token is handled by the runner environment, not the checkout action. Option 3 is wrong because checkout does not define the working directory path.

2. What happens if a step in a GitHub Actions job fails, and 'continue-on-error' is set to false (default)?

  • A.The subsequent steps in that job will skip, and the job status will be marked as failed.
  • B.The workflow pauses until the user manually restarts the specific step.
  • C.The job ignores the error and proceeds to the next step, but reports a warning.
  • D.The runner attempts to re-execute the step up to three times automatically.
Show answer

A. The subsequent steps in that job will skip, and the job status will be marked as failed.
The correct answer is that subsequent steps skip and the job fails, halting the pipeline. Option 1 is wrong because it does not pause automatically. Option 2 describes the 'continue-on-error' behavior. Option 3 is wrong because no automatic retry logic is built into the standard step execution.

3. Which of the following is the most secure way to handle a database password in your GitHub Actions workflow?

  • A.Include the password as a plain text string in the YAML file.
  • B.Store the password in a GitHub Environment Secret and call it using ${{ secrets.DB_PASS }}.
  • C.Export the password as a shell variable in an 'echo' command within the 'run' block.
  • D.Commit an encrypted '.env' file to the repository and decrypt it at runtime.
Show answer

B. Store the password in a GitHub Environment Secret and call it using ${{ secrets.DB_PASS }}.
The correct answer is using GitHub Secrets. Option 0 exposes the password to everyone. Option 2 is insecure because 'echo' commands can be logged. Option 3 is a security risk if the decryption key is not managed correctly; Secrets are the intended native solution.

4. If you want a workflow to run only when a Pull Request is opened, which event configuration should you use?

  • A.on: push
  • B.on: pull_request: types: [opened]
  • C.on: workflow_run: types: [created]
  • D.on: repository_dispatch
Show answer

B. on: pull_request: types: [opened]
The correct answer is 'pull_request' with the 'opened' type filter. Option 0 triggers on direct commits, not PR creation. Option 2 is for workflow orchestration. Option 3 is for triggering workflows via API calls, not PR events.

5. What is the primary benefit of using a 'matrix' strategy in a GitHub Actions job?

  • A.It allows you to store multiple passwords in a single secret.
  • B.It creates multiple instances of a job to test across different OS versions or configurations.
  • C.It compresses all job logs into a single summary file.
  • D.It allows the workflow to use multiple different runner platforms at the exact same time inside one step.
Show answer

B. It creates multiple instances of a job to test across different OS versions or configurations.
The correct answer is running jobs across multiple configurations (e.g., Ubuntu, macOS, Windows). Option 0 is incorrect as secrets are simple key-value pairs. Option 2 is wrong because logs are kept separate. Option 3 is wrong because a single step can only run on one platform at a time.

Take the full Git & GitHub quiz →

← PreviousPull Requests and Code ReviewNext →Branch Protection Rules

Git & GitHub

20 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app