DEV Community

Martzcode
Martzcode

Posted on

"What I Wish I Knew About GitHub (and Especially GitHub Actions)"

When I started using GitHub, I treated it as a place to store code. Years later, I realized it's also a build server, a release manager, a deployment platform, and an automation engine, all included in the same repo.

This is the article I wish I had read at the beginning. First, a quick tour of the commands worth knowing. Then the main event: GitHub Actions and everything you can automate with it.


Part 1: GitHub essentials (the short version)

Git commands I use every day

# Start from a remote repo
git clone https://github.com/user/repo.git

# Work on a feature branch (never directly on main)
git switch -c feature/login-page

# See what changed
git status
git diff

# Stage and commit
git add -p                      # stage interactively, hunk by hunk
git commit -m "feat: add login page"

# Push and set upstream
git push -u origin feature/login-page

# Stay up to date
git pull --rebase origin main
Enter fullscreen mode Exit fullscreen mode

Git commands I wish I had learned earlier

# Undo the last commit but keep your changes
git reset --soft HEAD~1

# Temporarily save work in progress
git stash push -m "wip: login form"
git stash pop

# Find which commit introduced a bug (binary search!)
git bisect start
git bisect bad                  # current commit is broken
git bisect good v1.2.0          # this tag was fine
# Git checks out commits for you to test, then:
git bisect reset

# Recover "lost" commits after a bad reset or rebase
git reflog

# Work on two branches at once, in two folders
git worktree add ../repo-hotfix hotfix/urgent

# Tag a release
git tag -a v1.0.0 -m "First stable release"
git push origin v1.0.0
Enter fullscreen mode Exit fullscreen mode

The GitHub CLI (gh): a game changer

The GitHub CLI lets you do almost everything from the terminal.

gh auth login                        # authenticate once

gh repo create my-app --public --clone
gh pr create --fill                  # open a PR from your current branch
gh pr list                           # list open PRs
gh pr checkout 42                    # test someone's PR locally
gh pr merge 42 --squash --delete-branch

gh issue create --title "Bug: login fails" --body "Steps to reproduce..."

gh release create v1.0.0 --generate-notes

gh run list                          # list recent workflow runs
gh run watch                         # follow a run live
gh run view --log-failed             # show logs of failed steps only
gh workflow run deploy.yml           # trigger a workflow manually
Enter fullscreen mode Exit fullscreen mode

The last four commands are the ones that connect to the second part of this article.

Repo features worth enabling early

  • Branch protection rules: require PR reviews and passing status checks before merging into main.
  • CODEOWNERS: automatically request reviews from the right people.
  • Issue and PR templates (.github/ISSUE_TEMPLATE/, .github/pull_request_template.md).
  • Dependabot: automatic dependency update PRs.
  • Secret scanning and push protection: catches leaked keys before they hit the repo.

Part 2: GitHub Actions, the real superpower

GitHub Actions is GitHub's built-in automation platform. You describe what should happen in a YAML file, and GitHub runs it on its own servers in response to events (a push, a pull request, a tag, a schedule, a button click...).

Public repositories get free minutes, and private repositories get a monthly free quota depending on your plan. Check the current limits in the GitHub docs.

The vocabulary in 60 seconds

Concept Meaning
Workflow A YAML file in .github/workflows/
Event / trigger What starts the workflow (push, pull_request, schedule...)
Job A group of steps running on the same machine (jobs run in parallel by default)
Step A single shell command (run) or a reusable action (uses)
Runner The machine that executes a job (ubuntu-latest, windows-latest, macos-latest, or self-hosted)
Action A reusable building block, found on the Marketplace

Your first workflow

Create .github/workflows/ci.yml:

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: 22
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

Commit, push, open the Actions tab. That's it: every push and every PR now runs your tests.

Tip: Combine this with branch protection and you can block merging until this workflow is green.


What you can automate with GitHub Actions

1. Continuous Integration with a build matrix

Test on several versions and operating systems at once:

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

That's 4 jobs from about 15 lines. The same idea works for Python, Java, Go, Rust, .NET and more.

2. Automatic releases

Option A: Release when you push a tag

name: Release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm run build
      - run: zip -r dist.zip dist

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create "${{ github.ref_name }}" dist.zip \
            --generate-notes \
            --title "${{ github.ref_name }}"
Enter fullscreen mode Exit fullscreen mode

Now releasing is just:

git tag v1.2.0 && git push origin v1.2.0
Enter fullscreen mode Exit fullscreen mode

GitHub builds your project, attaches the artifact, and generates release notes from merged PRs.

Option B: Fully automated versioning

Tools like release-please or semantic-release read your commit messages (Conventional Commits: feat:, fix:, feat!:) and automatically:

  • bump the version,
  • update the CHANGELOG.md,
  • open a "Release PR" (release-please) that you merge to publish.
name: release-please

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          release-type: node
Enter fullscreen mode Exit fullscreen mode

Publish a package to npm

name: Publish to npm

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write   # enables provenance
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

The same pattern works for PyPI, crates.io, NuGet, Maven Central, RubyGems and others.

3. Deploy a web app

Static site or SPA to GitHub Pages (free)

Works for React, Vue, Vite, Astro, Docusaurus, Hugo and more. In your repo, go to Settings → Pages → Source: GitHub Actions, then:

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4
Enter fullscreen mode Exit fullscreen mode

Vercel, Netlify, Cloudflare Pages, Azure, Firebase...

Most hosting platforms offer a CLI or an official action, and they all follow the same pattern: build, then deploy with a token stored in Secrets. Example with the Vercel CLI:

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm i -g vercel
      - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Enter fullscreen mode Exit fullscreen mode

Your own server over SSH

name: Deploy to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy over SSH
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          HOST: ${{ secrets.SSH_HOST }}
          USER: ${{ secrets.SSH_USER }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "$HOST" >> ~/.ssh/known_hosts
          ssh "$USER@$HOST" "cd /var/www/app && git pull && npm ci && npm run build && pm2 restart app"
Enter fullscreen mode Exit fullscreen mode

Cloud providers without long-lived secrets (OIDC)

Instead of storing AWS/Azure/GCP keys as secrets, GitHub can give your workflow a short-lived token via OpenID Connect. Example for AWS:

permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/checkout@v4
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-deploy
      aws-region: eu-west-3
  - run: aws s3 sync ./dist s3://my-bucket --delete
Enter fullscreen mode Exit fullscreen mode

No stored access key means nothing to leak or rotate.

4. Build and push Docker images

GitHub also hosts a container registry (GHCR):

name: Docker

on:
  push:
    tags: ["v*"]

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
Enter fullscreen mode Exit fullscreen mode

5. Deployment environments with manual approval

Define environments (staging, production) in Settings → Environments. You can add required reviewers, wait timers, and environment-specific secrets.

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "Deploying to staging..."

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production   # pauses until a reviewer approves
    steps:
      - run: echo "Deploying to production..."
Enter fullscreen mode Exit fullscreen mode

Production deployments now wait for a human click. Very useful in teams.

6. Scheduled jobs (cron) and manual triggers

name: Nightly

on:
  schedule:
    - cron: "0 3 * * *"     # every day at 03:00 UTC
  workflow_dispatch:         # adds a "Run workflow" button
    inputs:
      environment:
        description: "Target environment"
        type: choice
        options: [staging, production]
        default: staging

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/nightly-report.sh
        env:
          TARGET: ${{ inputs.environment || 'staging' }}
Enter fullscreen mode Exit fullscreen mode

Use it for backups, link checkers, data scraping, report generation, or dependency audits. You can also run it from the terminal with gh workflow run nightly.yml -f environment=production.

7. Automate your pull requests and issues

Actions can react to almost anything that happens in your repo.

name: PR automation

on:
  pull_request:
    types: [opened, reopened]

permissions:
  pull-requests: write
  contents: read

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      # Labels PRs based on the files changed (configured in .github/labeler.yml)
      - uses: actions/labeler@v5

      - name: Welcome comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr comment ${{ github.event.pull_request.number }} \
            --repo ${{ github.repository }} \
            --body "Thanks for the PR! A maintainer will review it soon."
Enter fullscreen mode Exit fullscreen mode

More ideas:

  • Auto-close stale issues with actions/stale.
  • Auto-merge Dependabot PRs when tests pass.
  • Enforce PR title conventions (Conventional Commits).
  • Post preview URLs as a comment on every PR.
  • Run a code formatter and push the fix.

8. Security scanning

  • CodeQL: GitHub's code analysis, enabled in a few clicks or with github/codeql-action.
  • Dependency review: actions/dependency-review-action blocks PRs that add vulnerable dependencies.
  • Trivy, Snyk, Semgrep and other scanners all have ready-to-use actions.

9. Speed and cost: caching, artifacts, concurrency

# Cancel outdated runs when you push again
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Cache anything (here: pip packages)
      - uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}

      - run: pip install -r requirements.txt && pytest --junitxml=report.xml

      # Keep files from the run (test reports, builds, screenshots)
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-report
          path: report.xml
Enter fullscreen mode Exit fullscreen mode

10. Reuse your workflows

Copy-pasting YAML across 20 repos gets painful fast. Two tools fix this.

Reusable workflow (called from other workflows):

# .github/workflows/reusable-test.yml (in a shared repo)
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: "22"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test
Enter fullscreen mode Exit fullscreen mode
# In any other repo
jobs:
  call-tests:
    uses: my-org/shared-workflows/.github/workflows/reusable-test.yml@main
    with:
      node-version: "20"
Enter fullscreen mode Exit fullscreen mode

Composite action (a mini-action made of steps, in action.yml) and even custom JavaScript or Docker actions if you need full control. You can publish them to the Marketplace.


Best practices (learned the hard way)

  1. Least privilege for the token. Set permissions: explicitly at the top of each workflow instead of relying on defaults.
  2. Never print secrets, and never run untrusted code with access to them. Be careful with pull_request_target.
  3. Pin third-party actions, ideally to a full commit SHA (uses: owner/action@<sha>), and let Dependabot update them:
   # .github/dependabot.yml
   version: 2
   updates:
     - package-ecosystem: github-actions
       directory: /
       schedule:
         interval: weekly
Enter fullscreen mode Exit fullscreen mode
  1. Avoid injecting untrusted input into run:. Pass values through env: rather than interpolating ${{ github.event.pull_request.title }} directly in a shell command.
  2. Use concurrency to cancel duplicate runs and avoid overlapping deployments.
  3. Set timeout-minutes on jobs so a stuck job doesn't burn your minutes.
  4. Keep workflows small. Split by concern (CI, release, deploy) and reuse with workflow_call.
  5. Test locally with act when iterating on a workflow, and use gh run view --log-failed to debug quickly.
  6. Check the latest versions of the actions you use (actions/checkout, setup-node, etc.), since major versions move regularly.

Where to go from here

If you're just starting, this is a good progression:

  1. Add a CI workflow (lint + tests) to one of your repos.
  2. Turn on branch protection so main requires a green build.
  3. Automate a release on tag push.
  4. Automate deployment (GitHub Pages is the easiest place to start).
  5. Add Dependabot and a security scan.
  6. Extract repeated logic into reusable workflows.

Once these are in place, your repo stops being just a folder of code. It becomes a small, automated delivery pipeline, and you'll wonder how you ever shipped without it.


What's the coolest thing you've automated with GitHub Actions? Share it in the comments, I'm always looking for new ideas.

Top comments (0)