Training

GitHub Actions

Chaos Creeps In

Even the best teams drift. Rules are forgotten, naming conventions slip, and documentation falls behind. Left unchecked, every project trends toward disorder. Chaos doesn’t arrive suddenly — it creeps in quietly with each rushed commit and skipped review.

Automation Restores Order

GitHub Actions transforms your rules into code. Every push and pull request becomes an opportunity to check structure, test logic, and protect quality automatically. With automation guarding the boundaries, consistency stops depending on memory and starts depending on design.

Introduction & Setup

Chaos Creeps In

Entropy is real. Repos grow, rules drift, quality slips. Left alone, small inconsistencies become real problems.

Automation Restores Order

Consistent, automatic checks keep work tidy, repeatable, and easy to teach. GitHub Actions is your lightweight rule engine that runs when things change.

Training Goals

  • Understand what GitHub Actions is and why it is useful

  • See how automation can respond when changes happen in a repository

  • Create and run a simple workflow that performs an automatic task

  • Recognize how automation supports consistency, organization, and reproducibility

GitHub Actions lets you define small, repeatable chores that run for you when something happens in your repository. For example, when someone pushes a change or opens a pull request, your workflow can check spelling, lint Markdown, or build a site. You describe the chore in a short YAML file in .github/workflows/. GitHub hosts the work for you. You get results right on the pull request, which encourages teams to keep standards high without extra meetings or manual checks.

Training Resources

This single page is intended to jump-start your exploration of GitHub Actions. To do the training, fork the following repository:

[showhide type="code_setup" hidden="yes" more_text="Show Setup Code" less_text="Hide Setup Code"]
#!/bin/bash
# ---------------------------------------------------------------------
# Clone your own fork of the GitHub Actions training repository
# Repository: https://github.com/cropandsoil/github-actions-training
# ---------------------------------------------------------------------

# 1. Set your GitHub username (edit this line)
GITHUB_USER="your-github-username"

# 2. Define repo name and URLs
REPO_NAME="github-actions-training"
FORK_URL="https://github.com/${GITHUB_USER}/${REPO_NAME}.git"
UPSTREAM_URL="https://github.com/cropandsoil/${REPO_NAME}.git"

# 3. Choose or create a working directory
cd ~/projects 2>/dev/null || mkdir -p ~/projects && cd ~/projects

# 4. Clone your fork
echo "Cloning your fork..."
git clone "$FORK_URL"
cd "$REPO_NAME" || exit 1

# 5. Confirm that origin points to your fork
echo "Checking remote origin:"
git remote -v

# 6. (Optional) Add the main class repository as 'upstream'
echo "Adding upstream (optional)..."
git remote add upstream "$UPSTREAM_URL" 2>/dev/null || echo "Upstream already set."

# 7. Make a quick test edit (optional)
# echo "# test edit" >> README.md

# 8. Commit and push to trigger GitHub Actions in your fork
# git add README.md
# git commit -m "Trigger GitHub Actions test run"
# git push origin main

# ---------------------------------------------------------------------
# Done! Any push to your fork will now trigger GitHub Actions.
# ---------------------------------------------------------------------

[/showhide]

What Are GitHub Actions?

GitHub Actions lets you automate work in your repository when something happens. You describe a workflow in YAML, save it under .github/workflows/, and GitHub runs it in a clean environment. The results appear as checks on commits and pull requests so teams get fast, repeatable feedback.

An example of how this works:

  1. You push a change or open a pull request (an event).
  2. A workflow listens for that event and starts a job on a runner.
  3. The job executes a series of steps, each step runs an action or a shell command.
  4. The run produces logs, checks, and optional artifacts you can download.

[showhide type="doc_events" hidden="yes" more_text="Show GitHub Actions Event Types" less_text="Hide GitHub Actions Event Types"]

There are lots of other GitHub-native events that can trigger workflows besides cron and push/PR.

Here’s a quick map of the major categories with examples (all supported in on:):

  • Repo/branch/tag lifecycle: create, delete, fork, release, registry_package, page_build.

  • Issues & discussions: issues, issue_comment, discussion, discussion_comment. You can also filter to specific activity types with types:.

  • Pull-request ecosystem: pull_request (and activity types), pull_request_review, pull_request_review_comment, pull_request_target (special security considerations).

  • Checks & statuses: check_run, check_suite, status (useful for reacting to external CI results).

  • Security & dependency signals: code_scanning_alert, secret_scanning_alert, dependabot_alert, dependency_review.

  • Deployments & environments: deployment, deployment_status, workflow_run (trigger after another workflow finishes).

  • Pages & wiki: page_build, gollum (wiki edits).

  • Stars/watch: watch (stars), which can be handy for small automations.

  • Manual/custom/system-to-system:

    • workflow_dispatch (manual runs with inputs),

    • repository_dispatch (external systems ping your workflow with a custom payload),

    • workflow_call (reusable workflow invoked by another workflow).[/showhide]

     Core Vocabulary (learn these first)

    • Workflow: YAML file that defines when to run and what to do.

    • Event: Trigger that starts a workflow (e.g., push, pull_request, schedule).

    • Job: A group of steps that runs on a runner (can run in parallel or sequence).

    • Step: A single action or shell command inside a job.

    • Action: Reusable unit of work (e.g., actions/checkout@v4).

    • Runner / Runner image: The VM that executes your job (e.g., ubuntu-latest).

    • Check: Pass/fail status shown on commits and pull requests.

    • Artifact: Files saved from a run (reports, build outputs).

    • Secrets: Encrypted values available to jobs without exposing them in logs.

    • Permissions: Repo access granted to the workflow via the permissions: block.

    Useful next:

    • Context: Read-only data for expressions (e.g., github.ref, github.actor).

    • Expression: Logic inside ${{ }} for conditions/inputs (e.g., if:).

    • Environment variables (env:): Key–value pairs available to steps.

    • Matrix: Run the same job across variants (e.g., multiple Node versions).

    • Cache: Saved dependencies to speed up builds.

    • Concurrency: Prevent or cancel overlapping runs of the same workflow.

    Learning by Example

    Use these small workflows to learn by doing. Each exercise takes just minutes.

    Hello Workflow

    [showhide type="doc_hello" hidden="yes" more_text="Show Hello Workflow Details" less_text="Hide Hello Workflow Details"]

    Learning goal: see a workflow fire from a simple commit and locate the run output.

    What this teaches:

    • Where workflows live in the repo and how on: triggers work
    • How jobs and steps appear in the Actions UI
    • How to read the run log and find the commit SHA

    What to notice:

    • The file path .github/workflows/hello.yml is what makes it a workflow
    • The runner image is a clean VM for every run
    • The ${{ github.sha }} context gives traceability

    Common pitfalls:

    • Pushing to a branch that is not matched by the trigger
      Expecting environment to persist between runs

    Go further:

    • Add a second step that prints ${{ github.ref }} and ${{ github.actor }} to learn contexts

    Try it: edit a line in the workflow, commit, then open the Actions tab to see the run.

    [/showhide]

    [showhide type="code_hello" hidden="yes" more_text="Show Hello Workflow Code" less_text="Hide Hello Workflow Code"]
    name: Hello Workflow
    on:
      push:
        branches: [ main ]
    jobs:
      hello:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v4
          - name: Say hello
            run: |
              echo "Hello from GitHub Actions. Commit SHA: ${{ github.sha }}"
    
    [/showhide]

    Markdown Lint

    [showhide type="doc_markdown_lint" hidden="yes" more_text="Show Markdown Lint Details" less_text="Hide Markdown Lint Details"]

    Learning goal: experience a fast feedback loop on pull requests.

    What this teaches:

    • Pull request triggers and check results on the PR page
    • Reusable marketplace actions with pinned versions
    • Team norms enforced as automation, not nagging

    What to notice:

    • The paths: filter saves minutes by running only for docs changes
    • Failing checks block merges when branch protection is enabled

    Common pitfalls:

    • Forgetting to pin action versions to a major version
    • Large repos without paths: filters that run more than needed

    Go further:

    Add a .markdownlint.json to tune rules

    [/showhide]

    [showhide type="code_markdown_lint" hidden="yes" more_text="Show Markdown Lint Code" less_text="Hide Markdown Lint Code"]
    name: Markdown Lint
    on:
      pull_request:
        paths:
          - '**/*.md'
    jobs:
      mdlint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Run markdownlint-cli2
            uses: DavidAnson/markdownlint-cli2-action@v18
            with:
              globs: "**/*.md"
    
    [/showhide]

    Permissions Minimum

    [showhide type="doc_permissions_minimum" hidden="yes" more_text="Show Permissions Minimum Details" less_text="Hide Permissions Minimum Details"]

    Learning goal: set least-privilege permissions at the workflow level.

    What this teaches:

    • The permissions: block and why it matters for security
    • Grant write only when a job must write to issues or PRs
    • How permissions apply by default to all jobs in the file

    What to notice:

    • contents: read is usually enough for CI that only checks code
    • Jobs needing to comment on PRs require pull-requests: write

    Common pitfalls:

    • Adding a wide write permission globally when only one job needs it
    • Confusing job-level permissions with workflow-level defaults

    Go Further:

    • Compare with a version that tries to write a comment without the right scope

    [/showhide]

    [showhide type="code_permissions_minimum" hidden="yes" more_text="Show Permissions Minimum Code" less_text="Hide Permissions Miniumum Code"]
    name: Permissions Example
    on:
      pull_request:
    permissions:
      contents: read
      pull-requests: write   # only when you need to comment or set statuses
    jobs:
      demo:
        runs-on: ubuntu-latest
        steps:
          - run: echo "Least privilege illustrated"
    
    [/showhide]

    Python Tests

    [showhide type="doc_python_tests" hidden="yes" more_text="Show Python Tests Details" less_text="Hide Python Tests Details"]

    Learning goal: set up Python, cache dependencies, and run tests.

    What this teaches:

    • Using actions/setup-python and language-specific cache
    • The value of a lockfile for reproducible installs
    • Where to read test failures in logs

    What to notice:

    • Cache is keyed by Python version and dependency state
    • pytest -q keeps logs readable for beginners

    Common pitfalls:

    • Forgetting to include a requirements.txt or using floating dependencies
    • Cache keyed too broadly so stale packages leak across changes

    Go further:

    • Add coverage with pytest-cov and upload a small HTML report as an artifact

    [/showhide]

    [showhide type="code_python_tests" hidden="yes" more_text="Show Python Tests Code" less_text="Hide Python Tests Code"]
    name: Python Tests
    on:
      push:
      pull_request:
    permissions:
      contents: read
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-python@v5
            with:
              python-version: '3.12'
              cache: 'pip'
          - run: pip install -r requirements.txt
          - run: pytest -q
    
    [/showhide]

    Node CI

    [showhide type="doc_node_ci" hidden="yes" more_text="Show Node CI Details" less_text="Hide Node CI Details"]

    Learning goal: use Node with cache, tests, and linting when present.

    What this teaches:

    • actions/setup-node with automatic npm cache
    • Optional steps guarded by scripts that may or may not exist
    • How lint and unit tests complement each other

    What to notice:

    • npm ci uses the lockfile for deterministic installs
    • Lint often finds issues before tests do, which saves review time

    Common pitfalls:

    • Running npm install instead of npm ci in CI
    • Forgetting to commit package-lock.json

    Go further:

    • Add a matrix to test Node 18 and 20 for compatibility

    [/showhide]

    [showhide type="code_node_ci" hidden="yes" more_text="Show Node CI Code" less_text="Hide Node CI Code"]
    name: Node CI
    on:
      pull_request:
    permissions:
      contents: read
    jobs:
      ci:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
          - run: npm ci
          - run: npm test --if-present
          - run: npm run lint --if-present
    
    [/showhide]

    Artifact Demo

    [showhide type="doc_artifact_demo" hidden="yes" more_text="Show Artifact Demo Details" less_text="Hide Artifact Demo Details"]

    Learning goal: produce and keep a file from a run for later download.

    What this teaches:

    • When artifacts are useful, for example reports or build outputs
    • How to control retention and name artifacts clearly
    • How artifacts appear in the run summary

    What to notice:

    • Artifacts are scoped to a run, not to the repository
    • Small artifacts are ideal for homework checks or grading evidence

    Common pitfalls:

    • Uploading enormous folders without retention limits
    • Assuming artifacts are permanent

    Go further:

    • Attach a short Markdown report generated by the job and compare versions across runs

    [/showhide]

    [showhide type="code_artifact_demo" hidden="yes" more_text="Show Artifact Demo Code" less_text="Hide Artifact Demo Code"]
    name: Artifact Demo
    on:
      push:
    permissions:
      contents: read
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Generate report
            run: |
              mkdir -p reports
              echo "Run $GITHUB_RUN_NUMBER on $GITHUB_REF" > reports/summary.txt
          - name: Upload artifact
            uses: actions/upload-artifact@v4
            with:
              name: run-report
              path: reports/
              retention-days: 7
    
    [/showhide]

    Run Only When Files Change

    [showhide type="doc_run_only" hidden="yes" more_text="Show Run Only When Files Change Details" less_text="Hide Run Only When Files Change Details"]

    Learning goal: reduce CI noise by running only when relevant files have changed.

    What this teaches:

    • The concept of selective execution based on changed paths
    • The trade-off between completeness and speed
    • How to expose the changed file list in logs for debugging

    What to notice:

    • The action outputs lists you can branch on with if:
    • You can whitelist and blacklist patterns at the same time

    Common pitfalls:

    • Forgetting that renames and deletions also affect change sets
    • Over-restricting patterns and skipping valuable checks

    Go further:

    • Combine with artifact upload to produce a report only for docs edits

    [/showhide]

    [showhide type="code_changed_files" hidden="yes" more_text="Show Run Only When Files Change Code" less_text="Hide Run Only When Files Change Code"]
    name: Run Only When Files Change
    on:
      pull_request:
    permissions:
      contents: read
    jobs:
      selective:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - id: changed
            uses: tj-actions/changed-files@v45
          - name: List changed Markdown files
            if: steps.changed.outputs.any_changed == 'true'
            run: |
              echo "All changed: ${{ steps.changed.outputs.all_changed_files }}"
              echo "Changed .md:  ${{ steps.changed.outputs.all_modified_files }}"
    
    [/showhide]

    PR Labeler

    [showhide type="doc_pr_labeler" hidden="yes" more_text="Show PR Labeler Details" less_text="Hide PR Labeler Details"]

    Learning goal: automate triage using file-based labels.

    What this teaches:

    • pull_request_target versus pull_request and why the former is used here
    • How labeling enables filtered views and rules in repositories
    • Minimal write permissions for pull requests

    What to notice:

    • Label rules live in .github/labeler.yml which can evolve over time
    • Labels are a low-friction way to route reviews in larger classes or teams

    Common pitfalls:

    • Using pull_request_target to run untrusted code. This workflow uses a trusted action with a limited scope
    • Too many labels that become noise

    Go further:

    • Add an assignment rule that pings a maintainer for certain paths

    [/showhide]

    [showhide type="code_labeler" hidden="yes" more_text="Show PR Labeler Code" less_text="Hide PR Labeler Code"]
    # SPDX-License-Identifier: Apache-2.0
    name: PR Labeler
    on:
      pull_request_target:
        types: [opened, synchronize, reopened]
    permissions:
      contents: read
      pull-requests: write
    jobs:
      label:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/labeler@v5
            with:
              repo-token: ${{ secrets.GITHUB_TOKEN }}
              configuration-path: .github/labeler.yml
    
    [/showhide]

    Reusable Markdown Lint (callee)

    [showhide type="doc_reusable_workflow_callee" hidden="yes" more_text="Show Reusable Workflow (callee) Details" less_text="Hide Reusable Workflow (callee) Details"]

    Learning goal: define a shared workflow that other repos can call.

    What this teaches:

    • workflow_call with typed inputs and safe defaults
    • The idea of centralizing a rule once and reusing everywhere
    • Clear interfaces for repeatable automation

    What to notice:

    • Inputs with defaults allow call sites to be very short
    • The callee can evolve under version control like any other code

    Common pitfalls:

    • Forgetting to mark the file with on: workflow_call
    • Tight coupling to a specific repository layout

    Go further:

    • Add an output that returns a summary string the caller can post as a comment

    [/showhide]

    [showhide type="code_reusable_lint" hidden="yes" more_text="Show Reusable Workflow (callee) Code" less_text="Hide Reusable Workflow (callee) Code"]
    name: Reusable Markdown Lint
    on:
      workflow_call:
        inputs:
          globs:
            type: string
            required: false
            default: "**/*.md"
    permissions:
      contents: read
    jobs:
      lint:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: DavidAnson/markdownlint-cli2-action@v18
            with:
              globs: ${{ inputs.globs }}
    
    [/showhide]

    Call Reusable Markdown Lint (caller)

    [showhide type="doc_call_reusable_markdown_lint" hidden="yes" more_text="Show Call Reusable Markdown Lint (caller) Details" less_text="Hide Call Reusable Markdown Lint (caller) Details"]

    Learning goal: consume a shared workflow by path and ref.

    What this teaches:

    • Referencing another workflow with uses: and a commit, tag, or branch
    • Passing inputs through to the callee
    • The importance of version pinning for stability

    What to notice:

    • The caller is short and easy to read
    • The ref can be a tag for a stable teaching release

    Common pitfalls:

    • Using a moving branch for uses: and getting unexpected changes
    • Incorrect path to the callee file

    Go further:

    • Call the same callee twice with different inputs to compare results

    [/showhide]

    [showhide type="code_call_reusable" hidden="yes" more_text="Show Call Reusable Workflow (caller) Code" less_text="Hide Call Reusable Workflow (caller) Code"]
    name: Call Reusable Lint
    on:
      pull_request:
    permissions:
      contents: read
    jobs:
      docs:
        uses: cropandsoil/github-actions-training/.github/workflows/reusable-lint.yml@main
        with:
          globs: "**/*.md"
    
    [/showhide]

    Scheduled Maintenance

    [showhide type="doc_scheduled_maintenance" hidden="yes" more_text="Show Scheduled Maintenance Details" less_text="Hide Scheduled Maintenance Details"]

    Learning goal: run on a clock and by hand, independent of code changes.

    What this teaches:

    • Cron scheduling in UTC and manual workflow_dispatch
    • Lightweight housekeeping jobs that create a visible trace
    • How to log timestamps for audit

    What to notice:

    • Cron syntax is terse, include a comment with the human time
    • Scheduled jobs are ideal for reminders and cleanups

    Common pitfalls:

    • Assuming local time rather than UTC in cron expressions
    • Forgetting to test with workflow_dispatch

    Go further:

    • Add a matrix of times or days to compare queue wait times

    [/showhide]

    [showhide type="code_schedule_weekly" hidden="yes" more_text="Show Scheduled Maintenance Code" less_text="Hide Scheduled Maintenance Code"]
    name: Scheduled Maintenance
    on:
      schedule:
        - cron: '0 9 * * 1'   # Mondays 09:00 UTC
      workflow_dispatch:
    permissions:
      contents: read
    jobs:
      housekeeping:
        runs-on: ubuntu-latest
        steps:
          - run: echo "Weekly job ran at ${{ github.run_started_at }}"
    
    [/showhide]

    Inactivity Watchdog

    [showhide type="doc_inactivity_watchdog" hidden="yes" more_text="Show Inactivity Watchdog Details" less_text="Hide Inactivity Watchdog Details"]

    Learning goal: detect silence and notify maintainers without a code push.

    What this teaches:

    • Scheduled jobs that call the GitHub API using actions/github-script
    • Conditional steps based on calculated outputs
    • Optional email via SMTP secrets and a record kept in issues

    What to notice:

    • The check is based on pushed_at which includes merges into the default branch
    • The job writes a label and updates a single open reminder issue

    Common pitfalls:

    • Missing SMTP secrets, which is fine because the issue reminder still works
    • Forgetting to grant issues: write when creating or updating issues

    Go further:

    • Generalize the email recipient list using a repository secret instead of a hardcoded address

    [/showhide]

    [showhide type="code_inactivity_watchdog" hidden="yes" more_text="Show Inactivity Watchdog Code" less_text="Hide Inactivity Watchdog Code"]
    # .github/workflows/inactivity-watchdog.yml
    # SPDX-License-Identifier: Apache-2.0
    name: Inactivity Watchdog
    
    on:
      schedule:
        - cron: "7 12 * * *"     # every day at 12:07 UTC
      workflow_dispatch:          # allow manual runs
    
    permissions:
      contents: read
      issues: write
    
    env:
      MAINTAINER_EMAIL: maintainer@example.com   # <-- set to the primary maintainer
      INACTIVITY_DAYS: "7"
    
    jobs:
      check:
        runs-on: ubuntu-latest
        steps:
          - name: Compute days since last push
            id: since
            uses: actions/github-script@v7
            with:
              script: |
                // Language: JavaScript (Node.js via actions/github-script)
                const {owner, repo} = context.repo;
                const {data: r} = await github.rest.repos.get({owner, repo});
                const pushed = new Date(r.pushed_at);       // last push to default branch
                const now = new Date();
                const days = Math.floor((now - pushed) / (1000*60*60*24));
                core.info(`Last push: ${pushed.toISOString()} (${days} days ago)`);
                core.setOutput("days", String(days));
                core.setOutput("stale", days >= Number(process.env.INACTIVITY_DAYS) ? "true" : "false");
    
          # Optional email step (requires SMTP secrets). Skips cleanly if not configured.
          - name: Send inactivity email
            if: steps.since.outputs.stale == 'true' && env.MAINTAINER_EMAIL != ''
            uses: dawidd6/action-send-mail@v3
            with:
              server_address: ${{ secrets.SMTP_HOST }}        # e.g., smtp.office365.com
              server_port: ${{ secrets.SMTP_PORT }}           # e.g., 587
              username: ${{ secrets.SMTP_USERNAME }}
              password: ${{ secrets.SMTP_PASSWORD }}
              subject: "Repo inactive for ${{ steps.since.outputs.days }} days: ${{ github.repository }}"
              to: ${{ env.MAINTAINER_EMAIL }}
              from: "${{ secrets.FROM_NAME }} <${{ secrets.FROM_EMAIL }}>"
              secure: true
              priority: low
              body: |
                Heads up: ${{ github.repository }} has had no pushes or merges
                for ${{ steps.since.outputs.days }} days.
    
                Last push timestamp (UTC): ${{ github.event.repository.pushed_at || 'checked via API' }}
    
                Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
    
          # Always create or update a reminder issue so there is a record, even without SMTP
          - name: Open or update reminder issue
            if: steps.since.outputs.stale == 'true'
            uses: actions/github-script@v7
            with:
              script: |
                const title = "Inactivity reminder: no pushes in {{DAYS}} days"
                  .replace("{{DAYS}}", "${{ steps.since.outputs.days }}");
                const label = "inactivity-reminder";
    
                // ensure label exists
                try {
                  await github.rest.issues.getLabel({ ...context.repo, name: label });
                } catch {
                  await github.rest.issues.createLabel({ ...context.repo, name: label, color: "ededed" });
                }
    
                // find an open reminder
                const {data: issues} = await github.rest.issues.listForRepo({
                  ...context.repo, state: "open", labels: label
                });
    
                if (issues.length) {
                  await github.rest.issues.createComment({
                    ...context.repo, issue_number: issues[0].number,
                    body: `Still inactive at ${new Date().toISOString()} (run ${{ github.run_id }}).`
                  });
                } else {
                  await github.rest.issues.create({
                    ...context.repo,
                    title, labels: [label],
                    body: `This repository has had no pushes or merges for **${{ steps.since.outputs.days }}** days.`
                  });
                }
    
    [/showhide]

    ADR Policy Check (advanced)

    Purpose: discuss policy as code outside class time. Because this is an advanced topic and the implementation is long, inspect the file via the GitHub repository.
    Try it: skim the structure and talk about when to gate merges on policy.

    Save Time with Free Marketplace GitHub Actions

    Save time by starting with proven Actions from the GitHub Marketplace. Filter for free and verified publishers, skim recent release activity, then pin to a stable version and keep permissions minimal. Explore the curated Actions below, choose the one that matches your need, and drop its example YAML into your workflow to get value fast.

    GitHub Action Ideas

    TODO expiry nudge
    Add a comment when a TODO older than 30 days is found in a PR diff.
    Triggers: pull_request. Hints: scan changed files, parse dates, post comment. Permissions: pull-requests write.

    Conventional commits gate
    Fail the check if the PR title is not a valid Conventional Commit.
    Triggers: pull_request. Hints: regex on title, short error message with examples.

    PR size budget
    Warn when a PR adds more than N lines. Encourage smaller reviews.
    Triggers: pull_request. Hints: use changed-files to get stats, post a summary.

    Enforce CODEOWNERS review
    Block merge if paths owned by a team lack an approving review
    Triggers: pull_request. Hints: read CODEOWNERS, compare reviewers. Pair with branch protection.

    Docs spell check on demand
    Run spell check only when .md files change.
    Triggers: pull_request with paths: filter. Hints: reusable action, custom dictionary file.

    Lint workflow YAML
    Validate files in .github/workflows/ with actionlint.
    Triggers: pull_request. Hints: actionlint action or container, fail fast with clear output.

    License header audit
    Fail if modified files are missing an SPDX header.
    Triggers: pull_request. Hints: grep each changed file, allow a skip pattern, show a quick fix.

    Coverage threshold
    Upload coverage and fail if total falls below a target.
    Triggers: push and pull_request. Hints: parse coverage summary, job outputs for pass or fail.

    Dependency risk check
    Run dependency review and fail on known vulnerabilities above a chosen severity.
    Triggers: pull_request. Hints: use actions/dependency-review-action, link to the report.

    Nightly smoke test
    Run a minimal test suite at 02:00 UTC even if no code changed.
    Triggers: schedule. Hints: short test command, artifact with timestamp, alert only on failure.

    Get Free Actions

    Browse curated, no-cost Actions to lint, test, build, and publish. Start here when you want a ready-made building block.

    Actions Cheat Sheet

    Concise quick reference for syntax, triggers, contexts, and common job patterns. Handy printable one-pager.

    Starter Workflows

    Drop-in CI templates from GitHub. Good patterns for Node, Python, Docker, Pages, and more to save you time.