GitHub Actions Command
Structured guidance for designing, hardening, reviewing, and debugging GitHub Actions workflows.
Activation
/platform-skills:github-actions design # reusable workflow or job graph design
/platform-skills:github-actions security # OIDC, SHA pinning, token scoping, secrets hygiene
/platform-skills:github-actions review # production-readiness checklist for an existing workflow
/platform-skills:github-actions debug # diagnose a failing workflow or job
Interactive Wizard (fires when no mode is provided)
When invoked with no arguments, ask before proceeding:
Q1 — Mode?
What do you need?
1. design — reusable workflow, job graph, promotion pipeline
2. security — OIDC federation, SHA pinning, token scoping, secret hygiene
3. review — production-readiness checklist for an existing workflow file
4. debug — job failure, permission error, OIDC rejection, missing context
Enter 1–4 or mode name:
Q2 — Context (after mode selected):
- design:
Describe what the workflow should do — validate, build, promote, deploy? - security:
Paste the workflow file or describe the auth pattern you are using. - review:
Paste the workflow file or provide the path. - debug:
Paste the error output or describe the failure symptom.
Mode: design
Triggers: design, build workflow, create pipeline, reusable workflow, job graph, promote, deploy flow
Read references/github-actions.md before responding.
Step 1 — Classify the workflow type
| Type | When to use |
|---|---|
Reusable workflow (workflow_call) | Same job sequence needed across multiple repos or environments |
| Composite action | Same step sequence needed within job graphs — use /platform-skills:composite-actions |
| Standard workflow | One-off or repo-specific, not shared |
Step 2 — Apply the canonical job pattern
Every workflow should follow this job order:
validate → build → [test] → promote → [deploy]
validate: lint, format, policy gates, unit checks — fast, no credentialsbuild: image or artifact packaging — OIDC to registrypromote: update version pin or overlay in Git — triggers GitOps reconcilerdeploy: guarded apply only if not using GitOps
Keep jobs small and named by intent. If a job is hard to name, it is doing too much.
Step 3 — Reusable workflow structure
# .github/workflows/validate.yml — called by other workflows
on:
workflow_call:
inputs:
environment:
required: true
type: string
description: "Target environment (dev | staging | prod)"
secrets:
token:
required: true
jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.token }}
Step 4 — Promotion orchestration
Prefer updating Git over imperative deploys:
# promote job — updates the version pin in the GitOps repo
promote:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Update image tag
env:
NEW_TAG: ${{ needs.build.outputs.image_tag }}
run: |
sed -i "s|image:.*|image: ghcr.io/org/app:${NEW_TAG}|" \
deploy/overlays/${{ inputs.environment }}/kustomization.yaml
git config user.email "ci@org.com"
git config user.name "CI"
git commit -am "chore: promote app to ${NEW_TAG} in ${{ inputs.environment }}"
git push
Handoffs:
- Extracting repeated steps →
/platform-skills:composite-actions - Terraform plan/apply in the workflow →
/platform-skills:terraform - GitOps reconciler that picks up the promotion commit →
/platform-skills:gitops
Mode: security
Triggers: OIDC, pin, SHA, token, permissions, secrets, secure, harden, federation
Read references/github-actions.md before responding.
Repositories with Kubernetes manifests or Helm charts: the posture gate is /platform-skills:kubescape ci (signature-verified CLI install, fails PRs only on new findings). Do not use kubescape/github-action: its latest release bundles CLI v3.0.21 and its README pins @main.
OIDC federation — AWS
permissions:
id-token: write # required for OIDC token request
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: eu-north-1
IAM trust policy (scope to repo and branch):
{
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main"
}
}
}
OIDC federation — Azure
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
SHA pinning checklist
# Resolve a tag to its current commit SHA
gh api repos/{owner}/{repo}/git/refs/tags/{tag} \
--jq '.object.sha'
# For actions that use a commit SHA directly (not a tag ref):
gh api repos/{owner}/{repo}/commits/{tag} --jq '.sha'
Add the version as a comment so reviewers can audit without resolving the SHA manually:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Token scoping — minimum permissions per job
| Job type | Permissions needed |
|---|---|
| Read-only checkout | contents: read |
| Push a commit | contents: write |
| Create or comment on PR | pull-requests: write |
| Publish packages | packages: write |
| OIDC token request | id-token: write |
| Read Issues | issues: read |
Set permissions: {} at the workflow level to deny all, then grant per-job:
permissions: {} # deny all at workflow level
jobs:
build:
permissions:
contents: read
id-token: write
packages: write
Secret hygiene
- Store secrets in GitHub Environments, not at repository level, for deployment secrets
- Never print secrets: avoid
echo $SECRETinrun:steps - Mask dynamic secrets:
echo "::add-mask::${DYNAMIC_SECRET}" - Rotate long-lived tokens on a schedule; prefer OIDC to eliminate them
- Audit secret usage:
gh secret listandgh secret list --env <env>
Mode: review
Triggers: review, audit, checklist, is this workflow safe, production ready
Ask the user to paste the workflow file or provide the path. Then evaluate against:
CRITICAL (must fix before merge)
❌ External `uses:` with mutable tag (@v4, @main, @latest) — supply chain risk; pin to full SHA
❌ `permissions: write-all` or no permissions block — grants all scopes by default
❌ `${{ github.event.*.body }}` or PR title interpolated in run: — script injection risk
❌ Long-lived cloud credentials stored as secrets — replace with OIDC
❌ `pull_request_target` with checkout of the PR head — arbitrary code execution risk
WARNING (should fix)
⚠️ No concurrency group — parallel runs on same branch can conflict or double-deploy
⚠️ No timeout-minutes on jobs — a hung job blocks runners indefinitely
⚠️ Secrets passed to untrusted third-party actions — scope secrets to first-party or audited actions only
⚠️ Environment protection rules not used for production deploys
⚠️ No required status checks on the branch protection rule this workflow targets
INFORMATIONAL
ℹ️ No dependabot.yml for github-actions ecosystem — action versions will drift
ℹ️ Reusable workflow could replace copy-pasted job sequence
ℹ️ Job name does not describe intent clearly
Report format:
## Review: <workflow-name>
### CRITICAL (N findings)
❌ Line 12: uses: actions/checkout@v4 — pin to SHA. Current SHA: <sha>
### WARNING (N findings)
⚠️ No concurrency group defined
### INFORMATIONAL (N findings)
ℹ️ No dependabot.yml for the github-actions ecosystem
### Score: N/15 — Poor | Fair | Good | Excellent
Mode: debug
Triggers: failing, error, permission denied, OIDC, 401, 403, missing context, debug, not working
Identify the failure layer first:
1. Syntax / parse error → workflow YAML invalid
2. Permission / auth error → token scoping, OIDC trust, secret missing
3. Missing context → expression evaluates to empty, wrong event trigger
4. Action version mismatch → SHA pinned to a version with a breaking change
5. Runner environment → tool not available on runner image
6. Downstream service error → cloud API, registry, or deploy target failing
Diagnosis by error type
Resource not accessible by integration / 403
# Check the permissions block — add the missing scope
permissions:
contents: write # missing if you get 403 on git push
pull-requests: write # missing if you get 403 on PR comment
id-token: write # missing if OIDC token request fails
OIDC rejection (Not authorized to perform sts:AssumeRoleWithWebIdentity)
- Confirm
id-token: writeis set on the job - Check the IAM trust policy
subcondition matches the exact repo/branch/env - Confirm the workflow is running from the branch/environment specified in the condition
- For environment-scoped trust:
repo:org/repo:environment:production— job must useenvironment: production
# Decode the OIDC token to inspect claims (add a debug step temporarily)
- name: Inspect OIDC token claims
run: |
TOKEN=$(curl -sSfL -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" | jq -r '.value')
echo "${TOKEN}" | cut -d. -f2 | base64 -d 2>/dev/null | jq
Expression evaluates to empty
# Workflow inputs are empty in non-workflow_call events
# Guard with:
- if: ${{ inputs.environment != '' }}
Context access might be invalid
Caused by accessing a context that does not exist for the triggering event (e.g. github.event.pull_request on a push event). Check which contexts are available for the trigger.
Validation command
# Lint the workflow file locally before pushing — syntax, expressions, shell
actionlint .github/workflows/<workflow>.yml
# Audit the same file for security findings — a valid workflow can still be unsafe
zizmor --strict-collection .github/workflows/<workflow>.yml
# Check recent run failures with full logs
gh run list --workflow <workflow>.yml --limit 10
gh run view <run-id> --log-failed
actionlint and zizmor do not overlap. actionlint answers "is this workflow valid" (syntax, expression types, shellcheck on run:). zizmor answers "is this workflow safe" (template injection, credential persistence, unpinned uses:, over-broad permissions:, impostor commits). Run both. For the full audit catalogue, policy file, and CI gate: /platform-skills:zizmor.
Common mistakes
pull_request_target+ PR head checkout — runs with write permissions and access to secrets; combined with checkout of untrusted code this allows secret exfiltration. Never checkout${{ github.event.pull_request.head.sha }}in apull_request_targetworkflow.- Mutable action tags —
@v4can be rewritten by the action author; a supply chain compromise silently executes attacker code. Always pin to SHA. - Over-scoped
GITHUB_TOKEN— default permissions can be broad depending on repo settings. Always set an explicitpermissions:block. - Forgetting OIDC requires the job's environment to match the trust policy — if the trust uses
environment:production, the job must declareenvironment: productionor the token request is rejected. workflow_dispatchwith no input validation — inputs are free-text by default. Usetype: choicefor enum inputs and validate string inputs in the first step.
Reference
Full guidance: references/github-actions.md
For composite action scaffolding and review: /platform-skills:composite-actions
For workflow security auditing with zizmor (audit catalogue, zizmor.yml policy, auto-fix, CI gate, pre-commit): /platform-skills:zizmor
Examples:
examples/github-actions/terraform-cicd.yml— Terraform plan + apply with OIDCexamples/github-actions/container-build.yml— Docker build + GHCR pushexamples/github-actions/reusable-workflows/— reusable workflow patterns