composite-actions · git:20261018.5f00d39 · 2026-10-18 · sha256 2379a4a947a527ce
composite-actions git:20261018.5f00d39B
Immutable. This exact content never changes and is served at /api/v1/blob/2379a4a947a527ce.
---
name: composite-actions
description: 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.
argument-hint: "[generate|review|secure|test] [action.yml path or description]"
title: "Composite Actions Command"
sidebar_label: "composite-actions"
custom_edit_url: null
---
# Composite Actions Command
Generate production-ready composite GitHub Actions or audit existing ones against best practices.
## Activation
```
/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 commands
```
---
## Mode: generate
**Triggers:** 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.
### Interview — ask these questions in order
**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?
> 1. **New dedicated repo** — best for shared/public actions (Marketplace-publishable)
> 2. **Existing repo** — internal action, placed under `.github/actions/<name>/` or `actions/<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?
> 1. **SHA pinning** *(recommended — supply chain secure, immutable)*
> 2. **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?
> 1. AWS via OIDC (no long-lived keys — `id-token: write` required)
> 2. Azure via OIDC
> 3. Both
> 4. Neither
**Step 7 — Notifications and PR comments**
> Should this action send notifications or post PR comments?
> 1. Slack webhook notification
> 2. GitHub PR comment (requires `pull-requests: write`)
> 3. Both
> 4. 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.yml
```
Ask for confirmation before generating.
### Generated file contents
#### action.yml
Generate with:
- `name`, `description`, `author` filled from the interview
- All inputs from Step 4, with `description:` noting which are secrets
- All outputs from Step 5
- A validation step as the **first step** — validates all required inputs and enum constraints
- Log grouping (`::group::` / `::endgroup::`) around each logical phase
- All secrets passed through `env:` blocks — never `${{ inputs.secret }}` in `run:`
- `::add-mask::` on every secret value immediately after it is read
- `${{ github.action_path }}` for all file references
- `shell: bash` on every `run:` step
- All external `uses:` pinned per the chosen strategy
- `$GITHUB_STEP_SUMMARY` written in a final `if: always()` step
- `timeout-minutes:` on every network-bound step
- `branding:` block with an appropriate icon and color
#### README.md
Generate an awesome-docs-compatible README with:
```markdown
# <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)
```
#### .github/dependabot.yml
```yaml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
commit-message:
prefix: "chore(deps)"
labels:
- "dependencies"
```
#### .github/workflows/test-action.yml
A test workflow that:
- Triggers on `push` and `pull_request` paths matching the action directory
- Calls the action using a local path reference (`./` or `./.github/actions/<name>`)
- Runs a matrix covering all optional inputs
- Verifies all outputs with assertions
- Uses `act` instructions in a comment header
#### .github/workflows/release.yml
- Triggers on `push` to tags matching `v[0-9]+.[0-9]+.[0-9]+`
- Validates `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 missing`
- Fails the release when an external `uses:` is not pinned to a 40-character SHA
- Updates the floating major version tag
- Creates a GitHub release with auto-generated notes
### PR creation (existing repo)
After generating all files:
```bash
# 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 generate` on the README to add architecture flow, lifecycle loop, and field carousel diagrams.
---
## Mode: review
**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)
- External `uses:` with mutable tag (`@v4`, `@main`, `@latest`) — supply chain risk
- `run:` step missing `shell:` — error on many runners
- `${{ inputs.* }}` interpolated directly in `run:` commands — injection risk
**Input injection — safe vs unsafe:**
```yaml
# ❌ 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 accessed via `${{ secrets.* }}` inside the action — always empty, silent failure
**WARNING** (should fix)
- No input validation step
- Secret inputs not masked with `::add-mask::` after reading
- File references using `./` instead of `${{ github.action_path }}`
- Boolean inputs compared with `== true` instead of `== 'true'`
- No `$GITHUB_STEP_SUMMARY` written
- No `timeout-minutes` on network-bound steps
- No `id:` on steps that produce outputs
**INFORMATIONAL**
- No `branding:` block
- Missing `description:` on inputs or outputs
- No `dependabot.yml` for the actions ecosystem
- No test workflow present
Report 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>
```
---
## Mode: secure
**Triggers:** secure, harden, fix, pin actions, add shell
Fix an existing `action.yml` in place. Apply all CRITICAL and WARNING fixes automatically:
1. **Resolve SHA pins** — for each `uses:` with a mutable tag, call `gh api` to resolve to the current SHA and add the version as a comment
2. **Add `shell: bash`** — to every `run:` step missing it
3. **Move inputs to `env:` blocks** — replace `${{ inputs.* }}` in `run:` with env variable references
4. **Add `::add-mask::`** — on every step that reads a secret input
5. **Add input validation step** — inject a validation step as the first step if none exists
6. **Add job summary** — inject a `$GITHUB_STEP_SUMMARY` step if none exists
Report 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())
```
---
## Mode: test
**Triggers:** test, generate test, write test workflow, act
Generate a complete test workflow for an existing composite action.
Ask:
1. Path to the `action.yml`
2. Which inputs have meaningful test variants (matrix)
3. Any secrets needed — provide placeholder names
Generate:
- `.github/workflows/test-<action-name>.yml` — triggers on PR + push for the action's directory
- Matrix covering all optional inputs
- Output assertion steps
- `act` commands with correct flags for local testing
Also output:
```bash
# 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-run
```
---
## Mode: migrate
**Triggers:** 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:
1. **Scan for duplicates** — read all `*.yml` files and find step sequences that appear in 3 or more jobs
2. **Rank candidates** — sort by: (count × step-count); highest impact first
3. **Present candidates** — show the top 3 with:
```
Candidate: 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 build
```
4. **Pick one** — ask the user which candidate to extract (or "all")
5. **Run the `generate` interview** — pre-fill answers from the detected steps; ask only what cannot be inferred (action name, outputs, pinning strategy)
6. **Generate the composite action** — write to `.github/actions/<name>/`
7. **Rewrite callers** — replace the extracted steps in each workflow with a single `uses:` call
Report:
```
## 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 each
```
---
## Mode: publish
**Triggers:** publish, marketplace, submit to marketplace, list on marketplace
Walk through the GitHub Actions Marketplace publishing checklist and generate all required metadata.
### Step 1 — Verify prerequisites
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 characters
- [ ] `author:` 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 example
- [ ] At least one release tag (`v1.0.0`)
### Step 2 — Generate Marketplace metadata
If `branding:` is missing, suggest 3 icon/color combinations based on the action's purpose:
```yaml
# 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`
### Step 3 — Release checklist
```bash
# 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 5
```
### Step 4 — Submit to Marketplace
Go to: `https://github.com/<owner>/<repo>/releases/new`
1. Set tag to `v1.0.0`
2. Check "Publish this Action to the GitHub Marketplace"
3. Review the category suggestion
4. Click "Publish release"
After 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 tab
```
---
## Reference
Full 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-platform
- `examples/github-actions/composite-actions/notify-slack/` — Slack webhook, secrets flow
- `examples/github-actions/composite-actions/k8s-deploy/` — kubectl, EKS/AKS/GKE OIDC
- `examples/github-actions/composite-actions/terraform-plan/` — Terraform plan, PR comment
- `examples/github-actions/composite-actions/security-scan/` — Trivy, severity gate, annotations
- `examples/github-actions/composite-actions/release-tag/` — semver bump, `$GITHUB_OUTPUT` chaining
- `examples/github-actions/composite-actions/pr-comment/` — `github-script`, token scoping
- `examples/github-actions/composite-actions/setup-env/` — multi-runtime (Node/Python/Go), tutorial baseline