Articles
GitHub Actions: The Stuff Nobody Tells You
I work at GitHub and use Actions every day. I've also debugged YAML at 2am and pushed way too many commits just to fix a conditional. Here's what I wish someone had told me on day one.
GitHub Actions: The Stuff Nobody Tells You
I work at GitHub. I use Actions every day. I’ve also debugged YAML at 2am, watched the log viewer eat my browser, and pushed fifteen commits in a row just to figure out why a conditional wasn’t evaluating correctly.
I’m not here to tell you Actions is perfect. It’s not. The log viewer has made grown engineers question their career choices. The YAML expression syntax has a learning curve that feels more like a learning cliff. The push-wait-fail-repeat debugging loop can turn a five-minute fix into an afternoon-long hostage situation.
I know this because I’ve lived it. And I’ve watched thousands of developers live it too.
But here’s what I’ve also seen: most of the pain comes from patterns that are avoidable. Not all of it. Some of it is the platform catching up. But a lot of it is stuff that has solutions right now that people haven’t discovered yet because the easy path is to just keep copy-pasting YAML and suffering.
This article is the stuff I wish someone had told me on day one.
Stop Using the Log Viewer
I’m serious. The web UI for reading build logs is the single biggest source of frustration with Actions, and the fastest fix is to stop using it.
gh run view --log-failed
That’s it. Failed step, in your terminal, instantly. No clicking through three pages. No waiting for the browser to decide whether it wants to render today. No back button roulette.
If you want the full log:
gh run view --log
If you want to watch a run in real time:
gh run watch
The CLI is faster, searchable with grep, and doesn’t crash when your test suite outputs 50,000 lines. If you’re still clicking through the web UI to read build logs in 2026, this is your sign to stop.
The YAML Problem Is Real. Here’s How to Shrink It.
Every CI system ends up as “a bunch of YAML.” Actions is no exception. But there’s a difference between a 40-line workflow that does one thing clearly and a 400-line monster with nested conditionals, matrix strategies, and inline bash scripts that would make a shell programmer cry.
The 400-line monster happens because people don’t know about the two features designed to prevent it. Or they don’t use them.
Reusable Workflows
If you have the same CI steps across multiple repos, you’re probably copy-pasting workflows. Stop.
# .github/workflows/ci.yml
jobs:
build:
uses: your-org/.github/.github/workflows/build.yml@main # @main is fine for internal org repos you control
with:
node-version: '20'
secrets: inherit
One workflow, maintained in one place, called from everywhere. When you update it, every repo that uses it gets the update. No drift. No “wait, which repo has the latest version of our deploy script?”
Composite Actions
Reusable workflows are for whole pipelines. Composite actions are for steps, the building blocks.
# .github/actions/setup-project/action.yml
name: 'Setup Project'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
shell: bash
- run: npm run build
shell: bash
Now your workflow files read like sentences, not telenovelas:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm test
The YAML is still there. But it’s 12 lines, not 120. And when setup changes, you change it in one place.
Break the Push-Wait-Fail Loop
The most soul-crushing part of Actions debugging: you make a one-character change to a workflow file, push it, wait four minutes for a runner to spin up, and find out you missed a quote. Fourteen commits later, your git history looks like a cry for help.
act, a community-maintained tool, runs your workflows locally in Docker containers. Same environment, no push required.
act -j build
It’s not a perfect replica. Some GitHub-specific contexts don’t exist locally. But for “did I break the YAML” and “does my bash script actually work,” it cuts that feedback loop from minutes to seconds.
For simple syntax validation before you even run anything:
gh workflow view ci.yml
If the YAML has obvious issues, it’ll surface them quickly. It’s not a full linter, but it catches the basics without a push cycle.
The Marketplace Trust Problem (and What to Do About It)
Every uses: some-stranger/cool-action@v2 is code you didn’t write running with access to your repo and secrets. That’s a real security concern, and “just pin to a SHA” is the right answer that nobody follows.
Here’s what actually works:
- Pin to SHAs and let Dependabot manage updates:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Add the version in a comment so humans can read it. Dependabot will open PRs when new versions drop, and you can review the diff before updating.
-
Stick to GitHub-maintained actions when possible.
actions/checkout,actions/setup-node,actions/cache— these are maintained by the same team that builds the platform. They’re audited, tested, and updated. -
For everything else, read the source. It’s open source. If a marketplace action is a 20-line shell script wrapped in a Dockerfile, maybe just copy the 20 lines into a
run:step and own it. -
Use OpenSSF Scorecard to assess action maintainer practices before adopting.
Conditional Logic Without Losing Your Mind
The ${{ }} expression syntax is one of those things that’s simple until it isn’t, and then it’s baffling. The edge cases around string interpolation, truthiness, and type coercion have bitten everyone at least once.
A few survival rules:
# Always quote expressions in `if:`
if: ${{ github.event_name == 'push' }}
# Use fromJSON for booleans from inputs
if: ${{ fromJSON(inputs.deploy) == true }}
# Multi-condition? Use >- to fold newlines into spaces.
if: >-
${{ github.ref == 'refs/heads/main' &&
github.event_name == 'push' }}
And if your conditional logic is getting complex enough to need a flowchart, that’s a sign you need a reusable workflow with inputs, not more if: statements.
The case() function is a recent addition worth knowing about. Think of it as a switch statement for expressions. It replaces nested ternaries that nobody can read.
Let Copilot Write the YAML
I’m not going to pretend the YAML is fun to write. But in 2026, you don’t have to write most of it.
In VS Code with GitHub Copilot, describe what you want in a comment:
# Deploy to production on push to main, run tests first,
# cache node_modules, notify Slack on failure
Copilot generates the workflow. You review and adjust. It handles the syntax, the on: triggers, the runs-on:, the step ordering, so you focus on what the pipeline should do, not on remembering whether environment goes inside or outside jobs:.
For existing workflows, Copilot agent mode can refactor a 400-line YAML file into reusable workflows and composite actions. Tell it what you want, review the PR.
This doesn’t fix Actions. It fixes you having to wrestle with it directly.
What’s Actually Getting Better
I said I wouldn’t sugarcoat this, so let me be specific about what’s improved and what still needs work.
Better now:
- Job summaries, structured output instead of just log lines
- Larger runners, 64-core machines available, ARM runners in GA
- Required workflows, organization-wide enforcement without copy-paste
- Immutable actions, actions published to GHCR with provenance
case()and recent expression improvements
Still needs work:
- The log viewer. It’s improved, but it’s not where it needs to be for large builds. Use the CLI.
- Debugging experience.
acthelps, but first-party local execution would change everything. - The learning curve for expressions. The docs team has been steadily improving this, and it shows — but there’s still room to grow.
I’m not writing this to defend GitHub’s honor. I’m writing it because I’ve watched too many teams suffer through problems that have solutions, and those solutions aren’t reaching people fast enough.
The fundamentals are there. The platform works. But “works” and “pleasant” are different things, and the gap between them is where your afternoon disappears. These patterns close that gap. Not all the way. But enough.
About the Author: Andrea Griffiths is a Senior Developer Advocate at GitHub, where she helps engineering teams adopt and scale developer technologies. She's passionate about making technical concepts accessible—to both humans and AI agents. Connect with her on LinkedIn, GitHub, or Twitter/X. · Read in Spanish · 阅读中文版