| name | crete-reusable-action |
| description | Create a new reusable composite GitHub Action with well-defined inputs/outputs that can be used by customer workflows. |
When to Use This Skill
Use this skill when you need to:
- Extract repeated code patterns from workflows into reusable actions
- Create new composable actions for common DevSecOps tasks
- Refactor inline scripts from workflows into maintainable actions
- Build actions that integrate with external APIs or GitHub API
Action Types
Shell Script Actions
Use when:
- The logic is simple and linear
- You only need environment variables and basic bash commands
- Working with files, strings, or simple data transformations
- No complex data structures or API interactions needed
- The action depends on standard Unix utilities (jq, curl, grep, awk, sed)
Examples in this repo:
actions/build/env-pairs-to-dot-env/action.yaml - Converts env pairs to .env file
actions/build/dotenv-to-ghaction-env/action.yaml - Loads .env into GitHub Actions environment
actions/github/check-runs-create/action.yaml - Creates GitHub check run via curl
products/lookinglass/manage-records/to-github-actions-matrix/action.yaml - Processes YAML to matrix
JavaScript Actions
Use when:
- Complex data manipulation (JSON parsing, transformation, validation)
- Making multiple API calls with error handling
- Need to use GitHub Octokit for GitHub API operations
- Conditional logic with complex branching
- Need to generate formatted output (Markdown, tables, summaries)
- Working with async/await patterns
Examples in this repo:
actions/github/ghas-dependabot-metadata/action.yaml - Fetches Dependabot metadata and creates PR comments
actions/github/lint-gha-sha-version/action.yaml - Downloads and runs gha-fix CLI with complex setup
actions/build/openssf-dependency-metadata/action.yaml - Fetches deps.dev API and formats results
actions/messaging/slack-notification/action.yaml - Email-to-Slack mention conversion with API calls
IMPORTANT: JavaScript actions MUST use actions/github-script@vX with inline pure JavaScript.
- NO NPM dependencies or package.json files
- NO separate .js files (all code inline in action.yaml)
- Use Node.js built-in modules only:
fs, path, os, child_process, crypto, etc.
- GitHub Actions provides:
@actions/core, @actions/github, github context, fetch (Node 20+)
Directory Structure
General-Purpose Actions
actions/
├── {category}/ # build, docker, github, messaging, network
│ └── {action-name}/ # descriptive-kebab-case name
│ └── action.yaml # MUST be action.yaml (not action.yml)
Categories:
build/ - Build utilities, configuration, environment management
docker/ - Docker-specific operations (scanning, SBOM, registry)
github/ - GitHub API operations, workflow management, GHAS
messaging/ - Notifications (Slack, email, etc.)
network/ - Network utilities, runner operations
Product-Specific Actions
products/
├── {product}/ # lift, lookinglass
│ └── {domain}/ # manage-env, manage-repo, manage-issue, etc.
│ └── {action}/ # specific action name
│ └── action.yaml
Products:
lift/ - Environment management product
lookinglass/ - Repository management and issue-ops product
Action Template
Shell Script Action Template
name: {Action Name}
description: {Clear description of what this action does}
author: {team-name or vionix}
branding:
icon: anchor
color: blue
inputs:
input-name:
description: "{Clear description of the input}"
required: true
default: ""
outputs:
output-name:
description: "{Clear description of the output}"
value: ${{ steps.step-id.outputs.output-name }}
runs:
using: composite
steps:
- name: {Descriptive step name}
id: step-id
shell: bash
env:
INPUT_VAR: ${{ inputs.input-name }}
run: |
# Shell script implementation
echo "Processing ${INPUT_VAR}"
echo "output-name=value" >> $GITHUB_OUTPUT
echo "## Summary" >> $GITHUB_STEP_SUMMARY
echo "Processed successfully" >> $GITHUB_STEP_SUMMARY
JavaScript Action Template
name: {Action Name}
description: {Clear description of what this action does}
author: {team-name or vionix}
branding:
icon: anchor
color: blue
inputs:
github-token:
description: "GitHub token for API access"
required: true
input-name:
description: "{Clear description}"
required: true
outputs:
output-name:
description: "{Clear description}"
value: ${{ steps.process.outputs.output-name }}
runs:
using: composite
steps:
- name: {Descriptive step name}
id: process
uses: actions/github-script@v8
env:
INPUT_VAR: ${{ inputs.input-name }}
with:
github-token: ${{ inputs.github-token }}
script: |
const core = require('@actions/core');
const github = require('@actions/github');
const fs = require('fs');
const path = require('path');
// Get inputs from environment
const inputVar = process.env.INPUT_VAR || '';
// Process data
const result = processData(inputVar);
// Set outputs
core.setOutput('output-name', result);
// Add to job summary
await core.summary
.addHeading('Summary', 2)
.addRaw(`Processed: ${result}`)
.write();
// Helper functions
function processData(input) {
// Implementation
return input.toUpperCase();
}
Composable Multi-Step Action Template
name: {Composite Action Name}
description: {Description of the cohesive problem this solves}
inputs:
input-1:
description: "First input"
required: true
input-2:
description: "Second input"
required: false
default: "default-value"
outputs:
final-output:
description: "The final result"
value: ${{ steps.combine.outputs.result }}
runs:
using: composite
steps:
- name: Prepare data
id: prepare
shell: bash
run: |
echo "Preparing ${{ inputs.input-1 }}"
echo "prepared=true" >> $GITHUB_OUTPUT
- name: Process with existing action
id: process
uses: Viasat/vionix/actions/category/action@main
with:
input: ${{ inputs.input-1 }}
- name: Combine results
id: combine
uses: actions/github-script@v8
env:
PREPARED: ${{ steps.prepare.outputs.prepared }}
PROCESSED: ${{ steps.process.outputs.result }}
with:
script: |
const prepared = process.env.PREPARED === 'true';
const processed = process.env.PROCESSED || '';
const result = prepared ? `Combined: ${processed}` : 'Failed';
core.setOutput('result', result);
Best Practices
1. Inputs/Outputs
- Always provide clear descriptions for all inputs and outputs
- Use
required: true for mandatory inputs
- Provide sensible defaults for optional inputs
- Output values should be strings (use JSON.stringify for objects)
- Use descriptive names (kebab-case for inputs, camelCase for outputs in JS)
2. Error Handling
Shell:
if [ ! -f "required-file.txt" ]; then
echo "ERROR: required-file.txt does not exist!"
exit 1
fi
JavaScript:
try {
const result = await riskyOperation();
core.setOutput('result', result);
} catch (error) {
core.setFailed(`Operation failed: ${error.message}`);
return;
}
3. Security
- Never log secrets or tokens
- Use
core.setSecret() to mask sensitive values in JavaScript
- Validate all inputs before use
- Use
github-token input instead of hardcoding secrets.GITHUB_TOKEN
Example:
const token = process.env.GITHUB_TOKEN || '';
if (token) core.setSecret(token);
4. Job Summaries
Always add meaningful summaries using $GITHUB_STEP_SUMMARY:
Shell:
echo "## Action Results" >> $GITHUB_STEP_SUMMARY
echo "- Status: Success ✅" >> $GITHUB_STEP_SUMMARY
echo "- Files processed: ${COUNT}" >> $GITHUB_STEP_SUMMARY
JavaScript:
await core.summary
.addHeading('Action Results', 2)
.addRaw('- Status: Success ✅\n')
.addRaw(`- Files processed: ${count}\n`)
.write();
5. Reusing Actions
Composite actions can call other actions:
- name: Use existing action
uses: Viasat/vionix/actions/build/dotenv-to-ghaction-env@main
with:
dot-file-path: .env
6. Testing Actions
- Test locally using
act or in a test workflow
- Create test workflows in
.github/workflows/test-*.yaml
- Use different scenarios (success, failure, edge cases)
Common Patterns
Pattern 1: File Processing
steps:
- name: Process files
shell: bash
run: |
for file in *.yaml; do
echo "Processing $file"
yq eval '.field' "$file"
done
Pattern 2: API Call with Retry
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Pattern 3: GitHub API Operations
const octokit = github.getOctokit(process.env.GITHUB_TOKEN);
await octokit.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Comment text'
});
const { data: files } = await octokit.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
Pattern 4: Multi-line Outputs
echo 'multiline-output<<EOF' >> $GITHUB_OUTPUT
echo "Line 1" >> $GITHUB_OUTPUT
echo "Line 2" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
const multilineValue = `
Line 1
Line 2
Line 3
`.trim();
core.setOutput('multiline-output', multilineValue);
Pattern 5: Conditional Execution
steps:
- name: Check condition
id: check
shell: bash
run: |
if [ "${{ inputs.enable-feature }}" == "true" ]; then
echo "enabled=true" >> $GITHUB_OUTPUT
else
echo "enabled=false" >> $GITHUB_OUTPUT
fi
- name: Run if enabled
if: ${{ steps.check.outputs.enabled == 'true' }}
shell: bash
run: echo "Feature is enabled"
Refactoring Candidates (TODO)
Below are inline code patterns found in existing workflows that should be extracted into reusable actions:
High Priority - Repeated Patterns
1. Git Repository Metadata Extraction
Found in: Multiple workflows (docker-compose-devsecops-workflow.yaml, etc.)
Pattern: Extracting git SHA, branch name, committer info
Proposed action: actions/github/extract-git-metadata/action.yaml
2. Docker Image Tag Generation
Found in: docker-compose-devsecops-workflow.yaml (lines 192-200)
Pattern: Generating Docker image tags from branch names and SHAs
Proposed action: actions/docker/generate-image-tags/action.yaml
3. Slack Notification Builder
Found in: Multiple workflows (279 shell runs for notifications)
Pattern: Building Slack payloads with job context
Proposed action: actions/messaging/build-slack-payload/action.yaml
4. Workflow Job URL Generator
Found in: Multiple workflows
Pattern: Generating job URLs for linking in notifications
Proposed action: actions/github/generate-job-url/action.yaml
5. YAML/JSON Validation
Found in: docker-compose-devsecops-workflow.yaml and others
Pattern: Validating YAML/JSON files with exclusion patterns
Proposed action: actions/build/validate-config-files/action.yaml
6. PR Context Fetcher
Found in: Multiple workflows using github-script inline
Pattern: Fetching PR number, title, author, labels
Proposed action: actions/github/fetch-pr-context/action.yaml
7. Artifactory Authentication
Found in: Multiple workflows with docker login commands
Pattern: Authenticating to Artifactory registry
Proposed action: actions/docker/artifactory-login/action.yaml
8. Build Timestamp Generator
Found in: docker-compose workflows
Pattern: Generating ISO timestamps for build metadata
Proposed action: actions/build/generate-timestamp/action.yaml
Medium Priority - Complex Workflows
9. SARIF Upload Handler
Found in: terraform-devsecops-workflow.yaml, security scanning workflows
Pattern: Uploading SARIF files to GHAS with error handling
Proposed action: actions/github/upload-sarif-results/action.yaml
10. Matrix Strategy Builder
Found in: Workflows with dynamic matrix generation
Pattern: Building GitHub Actions matrix from files or API responses
Proposed action: actions/github/build-matrix-strategy/action.yaml
11. Environment Variable Injector
Found in: Multiple workflows with env file handling
Pattern: Loading environment variables from various sources
Proposed action: actions/build/inject-environment-vars/action.yaml
12. Git Diff Analyzer
Found in: PR workflows checking changed files
Pattern: Getting changed files list, filtering by pattern
Proposed action: actions/github/analyze-git-diff/action.yaml
Low Priority - Specialized
13. SonarQube Reporter
Found in: Docker build workflows with sonarqube scanning
Pattern: Submitting scan results to SonarQube
Proposed action: actions/build/sonarqube-reporter/action.yaml
14. Checkout with Submodules
Found in: Multiple workflows with submodule handling
Pattern: Conditional checkout with submodule support
Proposed action: actions/github/checkout-with-options/action.yaml
15. Cache Key Generator
Found in: Workflows using actions/cache
Pattern: Generating cache keys from file hashes
Proposed action: actions/build/generate-cache-key/action.yaml
Creating a New Action - Step by Step
Step 1: Choose Location
- General purpose →
actions/{category}/{action-name}/
- Product-specific →
products/{product}/{domain}/{action}/
Step 2: Create Directory
mkdir -p actions/github/my-new-action
cd actions/github/my-new-action
Step 3: Create action.yaml
- Copy appropriate template from above
- Fill in name, description, author
- Define inputs with clear descriptions
- Define outputs with clear descriptions
Step 4: Implement Logic
- Shell: Keep it simple, use standard utilities
- JavaScript: Use github-script with inline code
- Add error handling
- Add job summary output
Step 5: Test
Create a test workflow:
name: Test My Action
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Test action
uses: ./actions/github/my-new-action
with:
input-name: test-value
- name: Verify output
run: echo "Output was ${{ steps.test.outputs.output-name }}"
Step 6: Document
- Add comments in action.yaml explaining complex logic
- Update CLAUDE.md if creating new category
- Add example usage in comments
Step 7: Version
- Tag the commit with semantic version
- Document breaking changes
- Update consuming workflows
Usage Examples
Using a Shell Action
- name: Convert env pairs to .env
uses: Viasat/vionix/actions/build/env-pairs-to-dot-env@v1.2.3
with:
dot-file-content: |
KEY1=value1
KEY2=value2
dot-file-artifact-name: build-env
dot-file-path: .env.build
Using a JavaScript Action
- name: Get Dependabot metadata
id: metadata
uses: Viasat/vionix/actions/github/ghas-dependabot-metadata@v2.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-enterprise: "true"
- name: Use metadata output
run: echo "Dependency: ${{ steps.metadata.outputs.dependabotMetadata }}"
Using a Composite Action
- name: Authenticate to GitHub
id: auth
uses: Viasat/vionix/products/lookinglass/manage-repo/authenticate@main
with:
authentication: app
application-id: ${{ secrets.APP_ID }}
application-private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Use authenticated token
run: |
echo "Token: ${{ steps.auth.outputs.token }}"
Tips & Tricks
1. Debugging Actions
Add debug output:
echo "::debug::Variable value is ${VAR}"
core.debug(`Variable value is ${var}`);
2. Working with JSON
echo '{"key":"value"}' | jq -r '.key'
jq -n --arg val "$VALUE" '{"key": $val}'
const obj = JSON.parse(jsonString);
const result = JSON.stringify({ key: 'value' });
3. File Operations
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync('file.txt', 'utf8');
fs.writeFileSync('output.txt', content, 'utf8');
if (fs.existsSync('file.txt')) {
}
fs.mkdirSync('new-dir', { recursive: true });
4. Environment Variables
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const temp = process.env.RUNNER_TEMP || '/tmp';
const eventName = process.env.GITHUB_EVENT_NAME;
5. Async Operations
const result = await asyncOperation();
const [result1, result2] = await Promise.all([
operation1(),
operation2()
]);
Resources
Remember: Actions should solve ONE cohesive problem. If your action does too many unrelated things, consider splitting it into multiple actions that can be composed together.