Copyright (c) 2026 MindMesh Academy. All rights reserved. This content is proprietary and may not be reproduced or distributed without permission.

5.1.3. Troubleshooting Action Execution

💡 First Principle: Action failures cluster into four causes — the action couldn't be resolved, its inputs didn't arrive as expected, its runtime environment was wrong, or its own code failed — and each announces itself with a distinctive message, so classification comes before investigation.

Resolution failures appear before any of the action's code runs: "Unable to resolve action owner/repo@ref, repository not found" usually means a private repository whose Access setting wasn't enabled (3.1.1), a typo, a deleted tag, or an organization policy blocking the source (3.1.2). A 404 on a private action is an access problem, not a missing action.

Input problems are quieter. Inputs arrive as INPUT_<UPPERCASED_NAME> environment variables with spaces converted to underscores, so a mismatch between action.yml and the code reading the variable yields an empty string rather than an error. Declaring required: true produces a warning, not a hard failure, in many paths — so validate inputs in your own code and fail loudly with core.setFailed().

Environment problems are type-specific. JavaScript actions fail with "Cannot find module" when dependencies weren't bundled (5.1.1), or with syntax errors when the runs.using Node major doesn't match the code's language level. Docker actions fail at image build (missing base image, failing RUN line) or with permission errors — the container runs as root by default while the runner's workspace files are owned by the runner user, a classic source of "permission denied" on mounted files. Composite actions fail when a run: step omits shell:, or when they reference secrets they were never passed.

Code failures are ordinary application bugs, and the tooling is the same as anywhere: core.debug() messages surfaced by ACTIONS_STEP_DEBUG (4.1.2), core.error()/core.warning() for annotations, and core.setFailed(message) — which both sets the failure exit code and prints the reason. Testing an action against its own repository with uses: ./ is the standard local-development loop.

⚠️ Exam Trap: core.setFailed() is what marks an action as failed; simply calling core.error() prints an annotation but leaves the exit code at zero, so the step passes. A scenario describing "errors in the log but the job is green" is this, or an unhandled promise rejection that never propagates.

Reflection Question: An action's input api-token reads as empty in the action's code, with no error anywhere. Give the exact environment variable name the runner sets, and the two places the mismatch could be.

See how it connects
Alvin Varughese
Written byAlvin Varughese
Founder18 professional certifications