Generate, review, secure, and test composite GitHub Actions following best practices — full repo scaffold, interview-driven generation, PR creation on existing repos, SHA pinning, secrets-as-inputs, job summaries, and actionlint validation.
71%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/composite-actions/SKILL.mdGenerate production-ready composite GitHub Actions or audit existing ones against best practices.
/platform-skills:composite-actions generate # interview → full repo scaffold → optional PR
/platform-skills:composite-actions review # audit an existing action.yml
/platform-skills:composite-actions secure # harden an action in place
/platform-skills:composite-actions test # generate a test workflow + act commandsTriggers: generate, create, new action, scaffold, write action, build action
Run a guided interview, then generate a complete, production-ready composite action with all supporting files. If the target repo already exists, open a PR.
Step 1 — Purpose
What does this action do? Describe it in one or two sentences. (e.g. "Build a Docker image and push it to GHCR using OIDC", "Send a Slack notification with build status and PR link")
Derive the action name from the description (kebab-case, under 30 chars). Confirm with the user.
Step 2 — Repo destination
Where should this action live?
- New dedicated repo — best for shared/public actions (Marketplace-publishable)
- Existing repo — internal action, placed under
.github/actions/<name>/oractions/<name>/
If existing repo: ask for owner/repo. Verify it exists with gh repo view owner/repo. Then ask:
Which subdirectory? (default:
.github/actions/<action-name>)
If the repo exists and is accessible, the action will be created on a branch and a PR will be opened automatically.
Step 3 — Pinning strategy
How should external actions be pinned?
- SHA pinning (recommended — supply chain secure, immutable)
- Semver floating tag (e.g.
@v4— easier to maintain, lower security)
Resolve SHAs for all external actions used via gh api repos/{owner}/{repo}/git/refs/tags/{tag} if SHA pinning is chosen.
Step 4 — Inputs
What inputs does this action need? For each input, collect:
- Name (snake_case)
- Type:
string/boolean/choice- Required or optional?
- If optional: default value
- Is it a secret? (webhook URL, kubeconfig, token, password, API key)
List all inputs in a table and confirm before proceeding.
Step 5 — Outputs
What values should this action expose as outputs? For each:
- Name (snake_case)
- Description (one sentence)
Step 6 — Cloud credentials
Does this action need cloud credentials?
- AWS via OIDC (no long-lived keys —
id-token: writerequired)- Azure via OIDC
- Both
- Neither
Step 7 — Notifications and PR comments
Should this action send notifications or post PR comments?
- Slack webhook notification
- GitHub PR comment (requires
pull-requests: write)- Both
- Neither
Step 8 — Job summary
Should this action write a job summary visible in the Actions UI? (Recommended: yes — adds a Markdown summary table with inputs, outputs, and status)
Step 9 — Confirm and generate
Show the user a summary of what will be generated:
Action: <name>
Description: <description>
Destination: <new repo | owner/repo/.github/actions/name>
Pinning: <SHA | semver>
Inputs: <count> (<N> are secrets)
Outputs: <count>
Cloud: <AWS OIDC | Azure OIDC | none>
Notifications: <Slack | PR comment | none>
Job summary: yes/no
Files to generate:
action.yml
README.md
CHANGELOG.md
.gitignore
scripts/<script>.sh (if logic warrants external scripts)
.github/dependabot.yml
.github/workflows/test-action.yml
.github/workflows/release.ymlAsk for confirmation before generating.
Generate with:
name, description, author filled from the interviewdescription: noting which are secrets::group:: / ::endgroup::) around each logical phaseenv: blocks — never ${{ inputs.secret }} in run:::add-mask:: on every secret value immediately after it is read${{ github.action_path }} for all file referencesshell: bash on every run: stepuses: pinned per the chosen strategy$GITHUB_STEP_SUMMARY written in a final if: always() steptimeout-minutes: on every network-bound stepbranding: block with an appropriate icon and colorGenerate an awesome-docs-compatible README with:
# <action-name>
> <one-line description>
<!-- To add animated diagrams to this README, run: /platform-skills:awesome-docs generate -->
## Architecture
*(Shows where this action fits in a CI/CD pipeline — add diagram with `/platform-skills:awesome-docs generate`)*
## Quick start
\`\`\`yaml
- uses: <owner>/<repo>@v1
with:
<required inputs with example values>
\`\`\`
## Inputs
| Input | Type | Required | Secret | Default | Description |
|---|---|---|---|---|---|
## Outputs
| Output | Description |
|---|---|
## Variables and secrets
Explain which inputs are secrets and how to wire them from the caller:
\`\`\`yaml
- uses: <owner>/<repo>@v1
with:
image_name: my-service # plain variable — safe to hardcode
webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} # secret — must come from secrets store
\`\`\`
## Permissions
\`\`\`yaml
permissions:
<minimum required permissions>
\`\`\`
## Idempotency
<Is it safe to re-run? What happens on a second run?>
## Concurrency (recommended)
\`\`\`yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: <true for build/validate, false for deploy/release>
\`\`\`
## Full example
\`\`\`yaml
<complete caller workflow showing all inputs>
\`\`\`
## Changelog
See [CHANGELOG.md](CHANGELOG.md)version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
commit-message:
prefix: "chore(deps)"
labels:
- "dependencies"A test workflow that:
push and pull_request paths matching the action directory./ or ./.github/actions/<name>)act instructions in a comment headerpush to tags matching v[0-9]+.[0-9]+.[0-9]+action.yml with yq (preinstalled on the runner, nothing downloaded): runs.using is composite, every step key is one a composite step accepts, and every run: step has shell:. Not actionlint: it lints workflows, and on action.yml it fails every release with "jobs" section is missinguses: is not pinned to a 40-character SHAAfter generating all files:
# Clone the target repo
gh repo clone owner/repo /tmp/action-scaffold
# Create feature branch
git -C /tmp/action-scaffold checkout -b feat/add-<action-name>-composite-action
# Write all generated files into the appropriate subdirectory
# Commit
git -C /tmp/action-scaffold add .github/actions/<name>/
git -C /tmp/action-scaffold commit -m "feat(actions): add <action-name> composite action"
# Push
git -C /tmp/action-scaffold push -u origin feat/add-<action-name>-composite-action
# Open PR
gh pr create \
--repo owner/repo \
--title "feat(actions): add <action-name> composite action" \
--body "$(cat <<'EOF'
## Summary
- Adds `<action-name>` composite action to `.github/actions/<name>/`
- Inputs: <list>
- Outputs: <list>
- Pinning: <SHA | semver>
- Secrets: <list of secret inputs>
## Test plan
- [ ] Test workflow passes on this branch
- [ ] Matrix covers all input combinations
- [ ] `actionlint` passes on the workflows, and the `yq` checks pass on `action.yml`
- [ ] `zizmor --strict-collection .` reports no medium or high findings
- [ ] Tested locally with `act`
- [ ] README inputs/outputs table reviewed
- [ ] Secret inputs confirmed to use `env:` isolation
## Usage
\`\`\`yaml
- uses: owner/repo/.github/actions/<name>@<sha>
with:
<example inputs>
\`\`\`
EOF
)"After PR creation, output:
PR opened:
<PR URL>Run
/platform-skills:awesome-docs generateon the README to add architecture flow, lifecycle loop, and field carousel diagrams.
Triggers: review, audit, check, lint my action, is this action safe
Audit an existing action.yml against the production checklist. Ask the user to paste or provide the path to the file.
Evaluate and report findings in three tiers:
CRITICAL (must fix before use)
uses: with mutable tag (@v4, @main, @latest) — supply chain riskrun: step missing shell: — error on many runners${{ inputs.* }} interpolated directly in run: commands — injection riskInput injection — safe vs unsafe:
# ❌ UNSAFE — input interpolated directly into shell, enables injection
- name: Deploy chart
shell: bash
run: helm upgrade --install payments-api ./chart --namespace ${{ inputs.namespace }}
# ✅ SAFE — input passed via env var; always quote the variable to prevent word-splitting
- name: Deploy chart
shell: bash
env:
NAMESPACE: ${{ inputs.namespace }}
run: helm upgrade --install payments-api ./chart --namespace "$NAMESPACE"Pass a secret input the same way, through env:, and send it in a request header. A token in a URL query string ends up in server and proxy logs.
This applies to ALL run: steps. Never interpolate ${{ inputs.* }}, ${{ github.event.* }}, or ${{ steps.*.outputs.* }} directly into shell commands.
${{ secrets.* }} inside the action — always empty, silent failureWARNING (should fix)
::add-mask:: after reading./ instead of ${{ github.action_path }}== true instead of == 'true'$GITHUB_STEP_SUMMARY writtentimeout-minutes on network-bound stepsid: on steps that produce outputsINFORMATIONAL
branding: blockdescription: on inputs or outputsdependabot.yml for the actions ecosystemReport format:
## Review: <action-name>
### CRITICAL (N findings)
❌ Line 14: `uses: actions/checkout@v4` — pin to SHA. Current SHA: <resolved SHA>
❌ Line 22: `run: deploy.sh --env ${{ inputs.environment }}` — injection risk. Use env: block.
### WARNING (N findings)
⚠️ No input validation step
⚠️ inputs.webhook_url not masked after reading
### INFORMATIONAL (N findings)
ℹ️ No branding: block — add icon and color for Marketplace
### Score: N/20 — <Poor | Fair | Good | Excellent>Triggers: secure, harden, fix, pin actions, add shell
Fix an existing action.yml in place. Apply all CRITICAL and WARNING fixes automatically:
uses: with a mutable tag, call gh api to resolve to the current SHA and add the version as a commentshell: bash — to every run: step missing itenv: blocks — replace ${{ inputs.* }} in run: with env variable references::add-mask:: — on every step that reads a secret input$GITHUB_STEP_SUMMARY step if none existsReport what was changed:
## Hardening applied to action.yml
✅ 3 actions pinned to SHA (actions/checkout, docker/setup-buildx-action, docker/build-push-action)
✅ shell: bash added to 2 steps
✅ inputs.webhook_url moved to env: block in step "Send notification"
✅ ::add-mask:: added for webhook_url
✅ Input validation step injected at position 1
✅ Job summary step injected (if: always())Triggers: test, generate test, write test workflow, act
Generate a complete test workflow for an existing composite action.
Ask:
action.ymlGenerate:
.github/workflows/test-<action-name>.yml — triggers on PR + push for the action's directoryact commands with correct flags for local testingAlso output:
# Local test commands
# Default inputs
act -W .github/workflows/test-<action-name>.yml \
-P ubuntu-latest=catthehacker/ubuntu:act-22.04
# With secrets
act -W .github/workflows/test-<action-name>.yml \
-P ubuntu-latest=catthehacker/ubuntu:act-22.04 \
--secret SLACK_WEBHOOK_URL=https://hooks.slack.com/test \
--secret KUBECONFIG=<base64-encoded>
# Dry run
act -W .github/workflows/test-<action-name>.yml --dry-runTriggers: migrate, extract, refactor workflows, consolidate steps, duplicate steps
Detect repeated step blocks across workflow files and extract them into a composite action.
Ask the user for the directory containing workflow files (default: .github/workflows/), then:
*.yml files and find step sequences that appear in 3 or more jobsCandidate: setup-node-and-cache (appears in 5 workflows, 4 steps each = 20 step-lines saved)
Steps:
- uses: actions/checkout@...
- uses: actions/setup-node@...
- run: npm ci
- run: npm run buildgenerate interview — pre-fill answers from the detected steps; ask only what cannot be inferred (action name, outputs, pinning strategy).github/actions/<name>/uses: callReport:
## Migration complete
✅ Extracted 4 steps → .github/actions/setup-node-and-cache/action.yml
✅ Replaced in 5 workflows:
- .github/workflows/ci.yml (steps 3–6)
- .github/workflows/deploy.yml (steps 2–5)
- .github/workflows/lint.yml (steps 1–4)
- .github/workflows/test.yml (steps 3–6)
- .github/workflows/release.yml (steps 4–7)
Lines saved: 20 step-lines across 5 files → 1 composite action call eachTriggers: publish, marketplace, submit to marketplace, list on marketplace
Walk through the GitHub Actions Marketplace publishing checklist and generate all required metadata.
Confirm each is met:
action.yml in the root of the repository (not a subdirectory — Marketplace requires root placement)name: is unique on the Marketplace (search https://github.com/marketplace?type=actions&query=<name>)description: under 125 charactersauthor: set (your GitHub username or org)branding: block present with icon: and color:README.md at root covers: what it does, inputs/outputs table, full examplev1.0.0)If branding: is missing, suggest 3 icon/color combinations based on the action's purpose:
# For a build/push action:
branding:
icon: 'box'
color: 'blue'
# For a security scan:
branding:
icon: 'shield'
color: 'orange'
# For a notification action:
branding:
icon: 'bell'
color: 'green'Valid Feather icons: activity, alert-circle, archive, bell, box, check-circle, cloud, code, cpu, database, download, eye, file, flag, git-branch, globe, heart, home, key, layers, lock, mail, monitor, package, play, plus, refresh-cw, search, send, server, settings, shield, star, tag, terminal, tool, trash, upload, user, users, zap
Valid colors: white, yellow, blue, green, orange, red, purple, gray-dark
# Tag the release
git tag v1.0.0
git push origin v1.0.0
# Create floating major tag
git tag -f v1
git push origin v1 --force
# Verify release workflow ran (action.yml check + SHA pinning check)
gh run list --workflow release.yml --limit 5Go to: https://github.com/<owner>/<repo>/releases/new
v1.0.0After publish, output:
## Marketplace listing
✅ Action published: https://github.com/marketplace/actions/<action-name>
Next steps:
- Add a Marketplace badge to README.md:
[](https://github.com/marketplace/actions/<name>)
- Set up dependabot to auto-update your own pinned SHA when you release new versions
- Watch for usage feedback in the Discussions tabFull documentation: references/composite-actions.md
For a security audit of the generated action.yml and its test/release workflows — template injection in run: steps, unpinned uses:, credential persistence, over-broad permissions: — run zizmor --strict-collection . or use /platform-skills:zizmor. Default collection already picks up action.yml alongside the workflows, so no --collect= flag is needed; --collect=all only adds paths your .gitignore excludes, at a real speed cost. Note that composite-action findings cannot be suppressed from .github/zizmor.yml; only inline # zizmor: ignore[rule] comments work there.
Examples:
examples/github-actions/composite-actions/docker-build-push/ — GHCR push, OIDC, multi-platformexamples/github-actions/composite-actions/notify-slack/ — Slack webhook, secrets flowexamples/github-actions/composite-actions/k8s-deploy/ — kubectl, EKS/AKS/GKE OIDCexamples/github-actions/composite-actions/terraform-plan/ — Terraform plan, PR commentexamples/github-actions/composite-actions/security-scan/ — Trivy, severity gate, annotationsexamples/github-actions/composite-actions/release-tag/ — semver bump, $GITHUB_OUTPUT chainingexamples/github-actions/composite-actions/pr-comment/ — github-script, token scopingexamples/github-actions/composite-actions/setup-env/ — multi-runtime (Node/Python/Go), tutorial baselinenitinjain999/platform-skills
c2e75b7
Also appears in
since Oct 19, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.