⚙️ GitHub Actions Tips That Will Save Your Sanity
Picture this: it's Friday at 4:57 PM. You push a "tiny" config change. The pipeline runs. The pipeline fails. You have no idea why because the error message says exit code 1 and absolutely nothing else. Your weekend plans evaporate. You are now the pipeline.
GitHub Actions is one of the most powerful CI/CD tools available today — it's baked into every GitHub repo, has a massive marketplace of community actions, and can automate almost anything. But "powerful" and "easy to debug at 5 PM on a Friday" are not the same thing.
These are the tips I wish someone had handed me before I spent three hours discovering that YAML indentation is not, in fact, optional.
1. Cache Your Dependencies Like Your Job Depends On It
Because it does. Every minute your pipeline spends downloading the same node_modules for the fifteenth time today is a minute you're burning GitHub's free tier minutes — and your patience.
- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
The magic here is hashFiles('**/package-lock.json'). The cache key is tied to your lockfile, so it busts automatically when dependencies actually change. No stale cache nightmares, no "but it worked on my machine" when someone adds a package.
For Python it's ~/.cache/pip, for Go it's ~/go/pkg/mod, and for Docker it's a whole other conversation (use cache-from and cache-to with type=gha). The pattern is the same: hash the lockfile, cache the directory, shave 2-4 minutes off every run.
On a team running 50 pipelines a day, that's hours of compute time and patience recovered every week.
2. Secrets Are Not Environment Variables (Treat Them Differently)
Here's a mistake I've seen in more repos than I'd like to admit:
# 🚫 Please don't do this
- name: Deploy
run: ./deploy.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
DEBUG: "true"
APP_ENV: "production"
TOTALLY_NOT_A_SECRET: "but what if it is"
The problem isn't using secrets — that's fine. The problem is mixing actual secrets with regular config and not thinking hard about what DEBUG: "true" means in a production deploy step.
A cleaner pattern: scope secrets to the jobs that actually need them, use environment-level protection rules in the GitHub UI, and for anything touching production, require manual approval:
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://yourapp.com
steps:
- name: Deploy
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
That environment: production line is doing real work. GitHub will enforce any protection rules you've set — required reviewers, wait timers, deployment branch restrictions. One config line and you've added a human checkpoint before anything touches prod.
3. Fail Fast, Fail Loud, Fail Informatively
The worst kind of pipeline failure is a silent one. You get exit code 1, a red X, and a 300-line log you have to grep through manually. Here's how to make failures actually tell you something:
- name: Run tests
run: |
set -euo pipefail
npm test 2>&1 | tee test-output.log
echo "Tests completed with status: $?"
- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-output.log
coverage/
retention-days: 7
set -euo pipefail is your friend. It makes bash exit immediately on any error (-e), treats unset variables as errors (-u), and propagates pipe failures correctly (-o pipefail). Without it, a failing command in a pipe can silently succeed and you'll never know.
The if: failure() block uploads artifacts when things go wrong — so instead of guessing what failed, you can download the full test report and read it at your leisure. Or your horror. Depends on the test results.
Real-World Lessons Learned (The Hard Way)
Lesson 1: Runner environment ≠ your laptop. That script that works perfectly locally? It breaks in CI because GitHub-hosted runners use Ubuntu 22.04 and your Mac uses zsh. Add #!/bin/bash to your shell scripts. Explicitly install the tools you need. Never assume.
Lesson 2: Workflow files are code. Review them like code. A YAML file that runs arbitrary commands on your production credentials deserves the same scrutiny as any other code. Require PR reviews on .github/workflows/. It's a 3-click setting in branch protection rules and it has saved teams from some spectacular own-goals.
Lesson 3: workflow_dispatch is criminally underrated. Adding a manual trigger to your workflows lets you re-run deploys, trigger one-off tasks, and debug pipeline issues without pushing a dummy commit. Add it to every workflow you own:
on:
push:
branches: [main]
workflow_dispatch: # ← just this
Now you can trigger the workflow from the GitHub UI, pass custom inputs, and stop littering your git history with "fix: trigger CI" commits.
Lesson 4: Matrix builds are great until you have 47 of them. Testing against 3 Node versions × 3 OS combinations × 5 database versions sounds thorough. It is also 45 parallel jobs that will eat your free tier minutes in 20 minutes. Be intentional. Test the combinations that actually matter.
The One Mindset Shift That Changes Everything
Stop thinking of your CI/CD pipeline as a "deploy button." Think of it as executable documentation of your release process.
Every manual step your team does before a release — running migrations, notifying Slack, updating a changelog, tagging a version — is a step that can be automated, codified, and reviewed. The pipeline is the source of truth for how software ships, not some tribal knowledge that lives in a Notion doc nobody reads.
When you write your workflows with that mindset, they stop being a chore and start being one of the most valuable pieces of infrastructure your team owns.
Your Next Steps
- Audit your current workflows — are you caching dependencies? If not, add it today. It's a 10-minute change with immediate payoff.
- Add environment protection rules to your production deployments in GitHub settings.
- Add
set -euo pipefailto every multi-linerun:block that doesn't have it. - Add
workflow_dispatchto your most-used workflows.
GitHub Actions rewards investment. The pipelines that are a joy to work with didn't get that way by accident — someone sat down and made them good. That someone can be you, and it's less work than you think.
Now go fix that pipeline before Friday.