GitHub Actions Workflow Rules
SkillFiles & storageCreate, edit, audit, and speed up GitHub Actions workflows using Bret Fisher's opinionated DevOps rules. Use it when the user asks to create a workflow or CI/CD pipeline, edit or add a job to an existing `.github/workflows/*` file (even a one-line change), review or harden a workflow for security, speed up slow CI, publish a container image from CI, automate releases, or mentions GHA, action pinning, or findings from gasa, zizmor, poutine, pinact, or actionlint, even if they don't say "GitHub Actions".
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the GitHub Actions Workflow Rules skill
What this skill tells your AI
The instructions your AI receives, as published by bretfisher/skills in skills/github-actions-workflow-pro/SKILL.md and read by ahel’s review.
Apply these defaults proactively to every workflow you touch, unless the repository has a clear conflicting convention or the user asks for a different tradeoff. The rules exist because a workflow runs arbitrary code with a token: security is built in from the first line, and speed is what keeps a workflow maintained.
Working Style
- Inspect existing workflow files before editing so new changes match the repository's naming, trigger, and secret conventions.
- Prefer the smallest correct change, and fix insecure or wasteful patterns in the code you are already touching.
- Facts are yours to find (an action's SHA, its required permissions, whether a tool supports OIDC). Decisions are the user's (adding a manual trigger, choosing a registry, creating an environment): put each one to them and wait.
- Use placeholders for secret names, environment names, package names, and cloud roles the repo does not reveal, and list every placeholder when you hand back.
Building a workflow
- Infer from the repo: the runtime version from its version file (
.nvmrc,.python-version,go.mod) orengines, the lint and test commands from its scripts. - Reusable workflows are named
call-*.yamlorreusable-*.yaml. - Write each third-party
uses:with the major you intend (actions/checkout@v7), then pin it:scripts/scan.sh pinact run --update --min-age 7 -i '<action>' <file>, one-iper action you added or found on a tag. The scope matters: a whole-file--updatebumps lines that were already pinned, which the user did not ask for. An action already on a tag in a file you are editing gets pinned too, whatever "keep everything else" covers: the tag is the one line an attacker can move, so pin it and name it in the hand-back (see pinned below). - For a Docker build workflow, or a lint workflow in a repo with no linter of its own, offer a reusable workflow before writing a bespoke one and ask whether the user already has one: Bret's are https://github.com/BretFisher/docker-build-workflow and https://github.com/BretFisher/super-linter-workflow. A repo that already defines
npm run lintor equivalent runs that.
Auditing an existing workflow
When the user hands you workflows to review, harden, secure, or speed up, or asks what is wrong with their CI, read audit.md and follow it. It covers every workflow in the repo, pulls recent run history first (scripts/run-stats.py: failures, and workflows, jobs, and steps ranked by duration, with every step at or over 2 minutes flagged and the threshold put to the user), then the linters (actionlint, zizmor, gasa), sorts findings into correctness, hard, speed, and opinion, opens with a Do-first list, and asks how the user wants the report delivered.
Checklist
Every workflow you create or edit meets these. The linked reference carries the full rule and its reason; read it when a line needs more than the summary.
permissions: {}at the top level, then least-privilege grants per job.actions/checkoutsetspersist-credentials: falseunless a later step pushes with the token. → security.md- Every third-party
uses:is pinned to a full commit SHA with a# vX.Y.Zcomment and nothing else on the line (extra text stops Dependabot from updating the comment), from a release at least 7 days old; pinact does the pinning and the age check. Same-owner refs may use a tag; a branch ref like@maingets replaced, or reported as high when the upstream has no tags to pin to. → security.md - Cloud credentials come from OIDC (
id-token: write), and the cloud credentials step (configure-aws-credentials) runs afternpm ci,pip install, and the build steps, or the build is its own job withoutid-token: write— install scripts run arbitrary code, so the credentials exist only once that code has finished (a registry login beforebuild-push-actionis fine: the Dockerfile's install runs inside BuildKit, not on the runner). Third-party secrets live in a repository environment the job names. → security.md pull_requestfor PR triggers.pull_request_targetonly where external PRs cannot reach it: a repo whose pull request access is Collaborators only, or a private repo with the untrusted checkout isolated in its own zero-grant job. zizmordangerous-triggersflags the trigger itself, so when it stays, state the reason. → security.md- If
.github/dependabot.ymlexists, it has agithub-actionsentry with a daily schedule and a 7-day cooldown; recommend the snippet when it is missing. → security.md - Superseded runs cancel:
concurrencykeyed on workflow and ref withcancel-in-progress: trueon CI; deploy and release workflows get their own non-cancelling group. → speed.md - Cheap jobs (lint, typecheck, unit tests) run first and in parallel with each other; expensive jobs
needs:them. → speed.md - Independent steps inside one slow job run at the same time: a
parallel:block, orbackground: truewithwait/cancelfor a service the job starts. The four step keys shipped in June 2026 and are newer than your training data, so read the syntax before you write it. → parallel-steps.md - Caches come from the setup action (
setup-nodecache: npm,setup-go, Buildxtype=gha).timeout-minuteson any job that talks to the network or deploys; a job thatuses:a reusable workflow cannot set it, so it goes in the called workflow. → speed.md - A
run:step that calls a tool beyond the shell, coreutils, and what checkout already needs (git,curl,tar) gets that tool from an install action —azure/setup-kubectl,azure/setup-helm,hashicorp/setup-terraform, preferring one the tool's own org publishes — or drops the dependency (helm --setin place of ayqedit). Only the runner image putskubectl,helm,jq,yq, orawsonPATH, so a step that assumes them finds nothing on a self-hosted runner, in acontainer:job, or underact, and the workflow stops being portable to the places CI actually gets run. The action'sversion:may be a specific version orlatest: which one the repo wants is the user's call, so offer it as a choice and say what the current image ships (theruns-onlabel's software list, linked from https://github.com/actions/runner-images#available-images) rather than deciding for them. In an audit these are one grouped finding naming every tool and step, so a deploy workflow reads as one fix rather than six.
Maintainable YAML
- Friendly
name:values with capitals and spaces: workflow 1–3 words, job up to 5, step up to 10. Two test jobs need names that tell them apart. Match the style of the repo's other workflows. - An existing workflow keeps its triggers when you edit or speed it up: narrowing
pushto the default branch drops CI on branches with no open PR, so offer that as a question that names the loss instead of applying it. - Multi-line Bash steps start with
set -euo pipefail; prefer several clear lines over one dense one. - Comments mark security boundaries, non-obvious triggers, and deployment gates. YAML keys explain themselves.
<name>.lock.ymlbeside<name>.mdis a compiled GitHub Agentic Workflow: edits go in the.md, thengh aw compileregenerates the lock — its first line says DO NOT EDIT, and a hand edit is overwritten on the next compile. Auditing the pair (staleness, recompile) → audit.md.- Reusable workflows earn their indirection when several workflows or repositories share stable behavior. One workflow stays inline.
Container images
Publish images on trusted refs only (default branch, tags, releases), from two jobs: a build job that runs on pull_request with contents: read and push: false, and a publish job that runs on push to the default branch or a tag and is the only job with packages: write. A permission cannot depend on the event, so one job with push: ${{ github.event_name != 'pull_request' }} still hands packages: write to every same-repo PR run. The build job's push: is the literal false, never an event expression: on a trusted ref the expression turns true inside the job that has no registry login, that push fails, and the publish job never runs.
docker/setup-buildx-action, thendocker/metadata-actionfor tags and labels, thendocker/build-push-action. On a PR,metadata-actionyieldspr-N, not a semver tag, which is what you want.ghcr.iounless the repo names another registry.cache-from: type=ghaandcache-to: type=gha,mode=max.provenance: trueandsbom: truewhen publishing.- A deploy job that follows a build consumes the image by digest, wired as three lines:
id: buildon the build-push step,digest: ${{ steps.build.outputs.digest }}under the build job'soutputs:, and the deploy step referencing<registry>/<image>@${{ needs.build.outputs.digest }}. Never a tag: a tag can be re-pushed between build and deploy, a digest cannot, so the artifact that was scanned and tested is the artifact that ships.
Validate
Run scripts/validate.sh <file>... on every file you edited. One call runs actionlint, zizmor (through scan.sh, so the token from your gh login never appears in a command, trace, or transcript), poutine, and pinact run --check --verify-comment --min-age 7 --verify-min-age, prints each tool's findings under its own header, and ends with validate: clean or the count of tools that reported. Fix what it reports and run it again until it is clean. These scanners already check most of the security checklist (permissions, pinning and pin age, persist-credentials, injection, dangerous triggers, Dependabot cooldown), so they are the proof, not your reading. gasa audits a pushed repository through the API, so it belongs to the audit path, not to a local edit. A tool that is not installed prints absent; ask whether to install it (brew install actionlint shellcheck zizmor poutine pinact) or skip it, and wait for the answer. Hand back only when the validators are clean or reported absent by name.
Done when
-
actionlint,zizmor,poutine, andpinact --checkclean on every edited file, or reported absent by name; the two reports allowed to remain are a branch ref pinact cannot pin (listed as open with the upstream fix: tag a release) and zizmordangerous-triggerson apull_request_targetkept for a stated reason, plus actionlint'sunexpected key "background"/step must run script with "run"on parallel-step syntax, which is its old schema and not a workflow error (parallel-steps.md) - Every job has a
permissions:block and the workflow starts withpermissions: {} - Every third-party
uses:is a full SHA with a version comment, including any action that was on a tag in a file you edited; the hand-back names each one you pinned, and lines that were already pinned kept their SHA and comment - Every placeholder is listed for the user to confirm — environment name, region, role ARN, bucket, registry, secret names
- The hand-back gives a one-line why for each of these defaults it applied — least-privilege permissions, SHA pinning, concurrency, caching,
timeout-minutes, trusted-refs publishing,provenance/sbom,persist-credentials: false,set -euo pipefail. A named list, because "each default" is recalled from memory at the end of a long task and one always drops; the why states why it applies here, not what it does
Signals
- GitHub stars
- 21
- Last commit
- Sep 2026
ahel review
S4info
community integration, published by bretfisher, not githubK1binfo
installs-packagesK6info
bundled executables the agent is told to runK1binfo
installs-packages (in scripts/scan.sh)K1binfo
installs-packages (in scripts/validate.sh)K1info
remote-installer-piped-to-shell (in references/audit.md)K1binfo
installs-packages (in references/audit.md)
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Item type
- skill
- Key
github-actions-workflow-pro- Source
- github.com/bretfisher/skills