DEV Community

Cover image for Your Code Runs Locally. GitHub Actions Makes It Run Everywhere, Automatically.
Nishant Gaurav
Nishant Gaurav

Posted on

Your Code Runs Locally. GitHub Actions Makes It Run Everywhere, Automatically.

You've pushed code to GitHub before. You know the drill: write something, commit it, push it, then manually do whatever comes next. Run the tests. Deploy the site. Update the docs. Every time, by hand.

GitHub Actions is the answer to "what if that next step just happened on its own?" It lets you define automated workflows that trigger whenever something happens in your repository. A push, a pull request, a schedule, even a manual button click. The workflow runs on GitHub's servers, not yours. You define the steps in a YAML file, commit it to your repo, and GitHub takes it from there.

This article walks through exactly how that works, using a real project as the example throughout: recLog, an automated pipeline that fetches articles from Dev.to and injects them into a portfolio's HTML without any manual update.


The Problem: Manual Steps After Every Push Don't Scale

Here's the situation that recLog was built to solve.

You write an article on Dev.to. You publish it. Now your portfolio is out of date because it lists your articles as static HTML that you maintain by hand. So you open your portfolio repo, copy in a new article card, adjust the HTML, commit, and push. Every. Single. Time.

This is fine once. It's annoying at ten articles. By twenty, you've already stopped doing it, which means your portfolio is quietly going stale while your actual writing keeps going.

The broader pattern is everywhere in software development. You push code and need to run tests before merging. You merge to main and need to deploy. You update a dependency and need to check if anything breaks. These are predictable, repeatable steps. Doing them manually introduces delay, inconsistency, and the very human tendency to skip them when you're in a hurry.

GitHub Actions turns these steps into automated workflows that live inside your repository. When the trigger fires, GitHub spins up a fresh machine, runs your defined steps in order, and reports back what happened. No server to maintain, no third-party CI tool to configure separately, no deploy keys to manage outside the repo.


How GitHub Actions Actually Works

The mental model

GitHub Actions is built around four concepts you need to understand before writing a single line of YAML: workflows, events, jobs, and steps.

A workflow is the top-level automation unit. It's a single YAML file that lives in .github/workflows/ inside your repository. One repo can have multiple workflow files, and each one is independent.

An event is what triggers the workflow. Push to main, open a pull request, publish a release, or run on a cron schedule. You define which events activate a given workflow in the on: block at the top of the file.

A job is a collection of steps that run together on the same machine. By default, jobs in the same workflow run in parallel. If one job depends on another, you declare that explicitly and GitHub sequences them accordingly.

A step is a single action within a job. It's either a shell command you write directly, or a pre-built action from the GitHub Actions marketplace that someone else wrote and you call by name.

Every job runs on a fresh virtual machine, called a runner, that GitHub provisions and discards after the job completes. This means each run starts from a clean state. Anything you need (dependencies, credentials, files from your repo) has to be explicitly set up within the workflow.

The workflow file

Here's the recLog workflow file, which is the actual file that keeps the portfolio in sync automatically:

# .github/workflows/main.yml

name: Sync Dev.to Articles

on:
  push:
    branches: [main]          # runs whenever code is pushed to main
  schedule:
    - cron: '0 */6 * * *'    # also runs every 6 hours automatically

jobs:
  sync:
    runs-on: ubuntu-latest    # GitHub provisions a fresh Ubuntu machine for this job

    steps:
      # Step 1: check out the repository so the runner has access to the code
      - name: Checkout repo
        uses: actions/checkout@v3

      # Step 2: install Python on the runner
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      # Step 3: install the project's dependencies
      - name: Install dependencies
        run: pip install -r requirements.txt

      # Step 4: run the pipeline (fetch, deduplicate, inject)
      - name: Run sync pipeline
        run: python main.py
        env:
          DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY }}  # pulled from repo secrets, never hardcoded

      # Step 5: commit and push the updated index.html back to the repo
      - name: Commit updated portfolio
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add index.html
          git diff --staged --quiet || git commit -m "chore: sync articles $(date -u +%Y-%m-%dT%H:%M:%SZ)"
          git push
Enter fullscreen mode Exit fullscreen mode

Walk through what this does. When code is pushed to main or when the 6-hour cron fires, GitHub provisions a fresh Ubuntu machine. It checks out the repository, installs Python 3.11, installs the dependencies from requirements.txt, runs main.py (which calls the Dev.to API, deduplicates articles against the existing HTML, and injects new cards), and then commits the updated index.html back to the repo. The portfolio updates without anyone touching it.

The git diff --staged --quiet || before the commit is worth noting. If main.py ran but found no new articles, index.html won't have changed. Without that check, the workflow would fail trying to commit a file with no changes. The || means: if the diff is empty (exit code 0), skip the commit.

How uses and run differ

You'll notice two kinds of steps in the file above.

Steps with uses: call a pre-built action. actions/checkout@v3 is maintained by GitHub and handles cloning your repo onto the runner correctly, including dealing with authentication. actions/setup-python@v4 installs Python and adds it to the runner's PATH. These are reusable building blocks from the Actions marketplace, versioned with the @v3 syntax so your workflow doesn't silently break when the action gets updated.

Steps with run: execute shell commands directly. This is where your own logic lives. Anything you'd type into a terminal, you can write here.

Secrets

Notice ${{ secrets.DEVTO_API_KEY }} in the workflow. This is how GitHub Actions handles credentials. You store the actual key value in your repository's Settings under Secrets and Variables, and reference it in the workflow using the ${{ secrets.NAME }} syntax. The value is never written to the workflow file, never visible in logs, and never committed to source control.

For recLog, this is where the Dev.to API key lives. The workflow reads it at runtime, passes it to the Python script as an environment variable, and the script uses it to authenticate against the Dev.to REST API.

The cron trigger

The schedule: event uses standard cron syntax. 0 */6 * * * means "at minute 0 of every 6th hour." GitHub runs this on UTC.

┌──── minute (0-59)
│  ┌─── hour (0-23)
│  │   ┌── day of month (1-31)
│  │   │  ┌─ month (1-12)
│  │   │  │  ┌ day of week (0-7, Sunday is 0 or 7)
│  │   │  │  │
0  */6  *  *  *
Enter fullscreen mode Exit fullscreen mode

For recLog, this means the portfolio checks for new articles four times a day without any manual trigger. If you publish at 2am, the site reflects it by 8am at the latest.


Where It Gets Tricky

The runner starts empty every time

The runner GitHub provisions do not know your previous runs. It doesn't have your dependencies cached, it doesn't have your environment variables set, it doesn't have any state from the last time the workflow ran. This catches beginners every time.

If your script writes a file during one run and expects that file to exist on the next run, it won't. Everything ephemeral needs to either be committed back to the repo (like recLog does with index.html), stored in an external service, or regenerated each run.

For recLog this was a deliberate design choice. The deduplication logic reads the current state of index.html in the repo to know which articles already exist. The runner checks out the repo fresh each time, runs the diff, and commits only changes. The repo itself is the state store.

Workflow permissions for committing back

By default, the GITHUB_TOKEN that GitHub provides to each workflow run has read-only access to the repository contents. If your workflow tries to git push without the right permissions, it will fail with a 403.

To fix this, go to your repository Settings, then Actions, then General, and set "Workflow permissions" to "Read and write permissions." Alternatively, you can set it per-workflow in the YAML:

permissions:
  contents: write
Enter fullscreen mode Exit fullscreen mode

This is one of the most common silent failure points for beginners writing their first workflow that commits back to the repo.

Cron schedules aren't guaranteed

GitHub's documentation states that scheduled workflows may be delayed during periods of high load on their infrastructure. A workflow scheduled for 6:00 UTC might run at 6:07, or occasionally later. For most use cases, this is fine. For anything where exact timing matters, scheduled Actions aren't the right tool.

There's also a lesser-known behaviour: GitHub will disable scheduled workflows on repositories that have had no activity for 60 days. If your portfolio repo goes quiet for two months, the sync stops. The fix is either to push something (anything) to keep the repo active, or to trigger the workflow manually from the Actions tab when you notice it's stopped.

Secrets in forks and pull requests

If someone forks your repo and opens a pull request, workflows triggered by that PR will not have access to your repository secrets. This is a security measure, not a bug. It means a fork-based PR can't exfiltrate your API keys. But it also means if your workflow requires a secret to run, it will fail on PRs from forks. Keep this in mind if you ever open-source a project that has required secrets in its workflow.


Building recLog's Pipeline Step by Step

Here's how to set up a workflow like recLog's from scratch, assuming you have a repository with a Python project that you want to automate.

Step 1: create the workflow directory

mkdir -p .github/workflows
Enter fullscreen mode Exit fullscreen mode

Step 2: write the workflow file

Create .github/workflows/main.yml and start with the trigger and job definition:

name: Sync Dev.to Articles

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 */6 * * *'

jobs:
  sync:
    runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode

Step 3: add the checkout and Python setup steps

These two steps appear in nearly every Python-based workflow. The checkout action clones the repo onto the runner. Without it, the runner has no access to your code.

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
Enter fullscreen mode Exit fullscreen mode

Step 4: install dependencies and run your script

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run sync pipeline
        run: python main.py
        env:
          DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY }}
Enter fullscreen mode Exit fullscreen mode

Step 5: add the secret to your repository

Go to your repository on GitHub. Settings, then Secrets and variables, then Actions, then "New repository secret." Name it DEVTO_API_KEY and paste in your Dev.to API key. The workflow will now have access to it at runtime.

Step 6: commit and push if the output changed

This is the step that closes the loop. The git diff --staged --quiet || guard ensures the commit only runs when there's actually something new to commit:

      - name: Commit updated portfolio
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add index.html
          git diff --staged --quiet || git commit -m "chore: sync articles $(date -u +%Y-%m-%dT%H:%M:%SZ)"
          git push
Enter fullscreen mode Exit fullscreen mode

Once this file is committed and pushed to main, GitHub picks it up automatically. You'll see the workflow appear in the Actions tab of your repository. Every run shows the status of each step, the logs, and how long it took.


What You Now Understand

A GitHub Actions workflow is not magic. It's a YAML file that tells GitHub: when this event happens, spin up a machine, run these steps in order, and report back. Every concept in that sentence is something you now have a working mental model of: events, runners, steps, secrets, and how to commit results back to the repo.

The recLog pipeline is a clean example of the pattern in its simplest useful form: trigger on push and on schedule, run Python, commit the output. That structure applies directly to dozens of other automation problems: syncing data, running tests, publishing packages, and generating reports.

Your next step is to open the Actions tab on any of your existing GitHub repositories and look at the starter workflows GitHub suggests. Pick the simplest one that applies to your stack, read through it line by line against what you've learned here, and run it. Seeing a green checkmark on your first workflow is how the mental model becomes a reflex.

Top comments (0)