developing-github-actions skill¶
Scope¶
Covers workflow YAML only. Does not cover authoring custom actions (JavaScript, Docker, or
composite actions). When a task requires writing action.yml or action implementation code,
say so, finish the workflow-side integration (calling and pinning the action), and treat the
action implementation as a separate task.
Rule of Thumb¶
If the repository already has workflows, follow their conventions: file layout, job and step naming, runner labels, action pinning policy, cache keys, trigger scoping, and how much logic lives in YAML versus scripts. If no workflows exist, apply the defaults in this skill and keep the first workflow small.
Workflow¶
1. Inspect¶
Read before writing:
.github/workflows/*.ymland*.yaml, including reusable workflows- Language and package manifests, lockfiles, build system
- Existing scripts,
Makefile, task runner configuration .github/dependabot.yml(thegithub-actionsecosystem entry)- CI expectations in
README,CONTRIBUTING, or docs actionlint.yamlorzizmor.ymlconfig if present- For failures: the actual run and logs, not only the YAML
2. Design¶
Decide each of these explicitly, then keep the workflow as simple as the requirement allows:
- trigger and trust boundary (who can run it, what it can access)
GITHUB_TOKENpermissions per job- job graph (
needs), runner, and matrix - caching, artifacts, concurrency
- environments, credentials, OIDC
- whether a reusable workflow or script extraction is justified
Do not add matrix, caching, concurrency, reusable workflows, or abstractions just because they are best practice. Each adds failure modes. Add them when a requirement or measurement justifies them.
3. Implement¶
- Prefer the repository's existing scripts and task runner over inline YAML logic.
- GitHub Actions should orchestrate; build and test logic belongs in commands a developer can run locally.
- Match existing action versions and pinning policy. Evaluate mutable refs (see security).
- Keep changes minimal; do not reformat or restructure unrelated workflow code.
4. Verify¶
Run the verification ladder after every change. Fix the first failing tier before continuing.
| Tier | Command | Catches |
|---|---|---|
| 1. Static | scripts/verify.sh |
syntax, expression types, context availability, needs, runner labels, action inputs, reusable workflow calls |
| 2. Security | scripts/verify.sh --security |
excessive permissions, injection, unpinned actions, dangerous triggers, credential exposure |
| 3. Repository | run the commands the workflow invokes | missing scripts, wrong flags, broken build or test |
| 4. Remote | gh run list, gh run view --log-failed |
real GitHub runner behavior and run history |
If a tool is unavailable, report that tier as not run rather than guessing. Do not claim a
workflow works because it looks correct. If a tier fails for a pre-existing reason unrelated
to your change (a broken lockfile, an empty test suite), report it; do not expand scope to fix
the project unless asked. See references/verification.md for tool
interpretation. gh cannot validate a workflow that has not been pushed.
Task Routing¶
Load only what the task needs.
| Task | Load | Covers |
|---|---|---|
| Create a workflow from scratch | authoring.md, triggers-and-expressions.md, then security.md |
structure, shell, env, outputs, if, needs; events, expressions, contexts; hardening |
| Add or change a job | authoring.md plus the reference for the changed area |
job and step mechanics; the changed area |
| Fix a failing run | debugging.md |
evidence, failure classification, gh, local reproduction limits |
| Review or harden | security.md |
threat model, injection, permissions, pinning, secrets, privileged triggers |
| Optimize | matrix-concurrency.md, caching-artifacts.md |
matrix and concurrency mechanics; cache and artifact mechanics |
| Reusable workflows | reuse.md |
workflow_call interface, permissions, limits |
| Deploy, environments, OIDC | deployments-and-oidc.md |
protection rules, deployment jobs, OIDC trust setup |
| Cache or artifacts | caching-artifacts.md |
cache keys and scope, artifact mechanics, data passing, trust |
| Expressions, contexts, variables | triggers-and-expressions.md |
event configuration, expression language, context availability, variables |
| Verification | verification.md |
verify.sh exit codes, actionlint and zizmor capabilities and limits |
Security Rules That Always Apply¶
- Never interpolate untrusted
${{ }}values intorun:orscript:bodies. Pass them throughenv:and reference the quoted shell variable. - Set
permissions:explicitly. Default tocontents: readand grant write scopes per job only where needed. pull_request_target,workflow_run,issue_comment, andissuesare privileged. Never check out untrusted code and execute it in these workflows.- Never write credentials into workflow YAML. Use
secrets, environment secrets, or OIDC. - Evaluate mutable action refs. Do not blindly pin everything to a SHA, but do not leave a third-party action on a tag or branch without a deliberate decision.
- Fork
pull_requestruns get a read-only token and no secrets by design. Do not switch topull_request_targetto "fix" that.
Explain the reasoning when reporting a security issue; do not just quote a rule.
Optimization¶
Do not add cache, concurrency, or matrix changes from pattern matching alone. When run data is
available, inspect duration, repeated jobs, superseded runs, matrix cost, cache behavior, and
the critical path first. When no run data is available, say so and ask for durations from the
Actions UI or gh run view; static duplication is a candidate, not proof. You may still fix
correctness and security issues, but do not claim a speedup without measurement. Optimize for
correctness, security, and feedback latency before runner minutes, and do not trade large
complexity for small time savings.