Skip to content

Instantly share code, notes, and snippets.

@askb
Last active October 25, 2025 01:01
Show Gist options
  • Select an option

  • Save askb/4b69882938f9ce8df7aab4319dc1c5b5 to your computer and use it in GitHub Desktop.

Select an option

Save askb/4b69882938f9ce8df7aab4319dc1c5b5 to your computer and use it in GitHub Desktop.
Generic MVP for creating new lfreleng-actions
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2025 The Linux Foundation
-->
# GitHub Actions MVP - Complete Guide
**Comprehensive Best Practices for Creating GitHub Actions**
This document synthesizes patterns from 43+ production GitHub Actions to provide a complete MVP (Minimum Viable Product) guide for creating robust, maintainable, and enterprise-ready GitHub Actions.
---
## Table of Contents
1. [Project Structure](#project-structure)
2. [Action Definition (action.yaml)](#action-definition-actionyaml)
3. [Licensing and Compliance](#licensing-and-compliance)
4. [Code Quality and Linting](#code-quality-and-linting)
5. [Documentation](#documentation)
6. [Testing and CI/CD](#testing-and-cicd)
7. [Implementation Patterns](#implementation-patterns)
8. [Security Best Practices](#security-best-practices)
9. [Versioning and Releases](#versioning-and-releases)
10. [Quick Start Template](#quick-start-template)
---
## Project Structure
### Standard Directory Layout
```
your-action-name/
├── .github/
│ ├── dependabot.yml # Automated dependency updates
│ ├── release-drafter.yml # Automated release notes
│ └── workflows/
│ ├── testing.yaml # Main test workflow
│ ├── tag-push.yaml # Release workflow
│ ├── semantic-pull-request.yaml # PR validation
│ └── sha-pinned-actions.yaml # Security audit
├── cmd/ # Go-based actions (optional)
│ └── your-tool/
│ └── main.go
├── docs/ # Documentation
│ └── conf.py # Sphinx configuration
├── pkg/ # Go packages (optional)
├── scripts/ # Helper scripts
├── src/ # Python source (optional)
│ └── your_package/
│ └── __init__.py
├── tests/ # Test files
├── .codespell # Codespell ignore list
├── .editorconfig # Editor configuration
├── .gitignore # Git ignore patterns
├── .gitlint # Git commit message linting
├── .pre-commit-config.yaml # Pre-commit hooks
├── .readthedocs.yml # ReadTheDocs configuration
├── .yamllint # YAML linting rules
├── action.yaml # Main action definition
├── LICENSE # Apache 2.0 license
├── LICENSES/ # REUSE compliance licenses
├── README.md # Main documentation
├── REUSE.toml # REUSE tool configuration
├── go.mod # Go dependencies (if applicable)
├── go.sum # Go checksums (if applicable)
├── pyproject.toml # Python project config (if applicable)
└── Makefile # Build automation (optional)
```
---
## Action Definition (action.yaml)
### Complete action.yaml Structure
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: 'Your Action Name 🎯'
description: 'Brief description of what your action does'
# Optional branding
branding:
icon: 'package' # Feather icon name
color: 'blue' # blue, green, orange, red, purple, gray-dark
inputs:
# Mandatory inputs
required_input:
description: 'Description of required input'
required: true
# type: string (commented - not used in composite actions)
# Optional inputs
optional_input:
description: 'Description of optional input'
required: false
default: 'default-value'
# Boolean inputs (as strings)
enable_feature:
description: 'Enable optional feature'
required: false
# type: boolean (treated as string in practice)
default: 'false'
# Path inputs
path_prefix:
description: 'Directory location containing project code'
required: false
default: '.'
# Numeric inputs (as strings)
timeout:
description: 'Operation timeout in seconds'
required: false
default: '300'
outputs:
result:
description: 'Description of the output'
value: ${{ steps.step-id.outputs.result }}
count:
description: 'Number of items processed'
value: ${{ steps.step-id.outputs.count }}
runs:
using: 'composite'
steps:
- name: 'Setup action/environment'
shell: bash
env:
INPUT_REQUIRED: ${{ inputs.required_input }}
INPUT_OPTIONAL: ${{ inputs.optional_input }}
run: |
# Setup action/environment
# Set input variables
required_input="${INPUT_REQUIRED}"
optional_input="${INPUT_OPTIONAL}"
# Validate inputs
if [ -z "$required_input" ]; then
echo 'Error: required_input is mandatory ❌'
exit 1
fi
# Output heading to step summary
echo '# 🎯 Your Action Name' >> "$GITHUB_STEP_SUMMARY"
# Set environment variables for subsequent steps
echo "processed_value=$required_input" >> "$GITHUB_ENV"
- name: 'Main action logic'
id: step-id
shell: bash
env:
INPUT_VALUE: ${{ inputs.required_input }}
run: |
# Main action logic
# Your implementation here
result="success"
count="42"
# Set outputs
echo "result=$result" >> "$GITHUB_OUTPUT"
echo "count=$count" >> "$GITHUB_OUTPUT"
# Update step summary
echo "Processing completed: $count items ✅" >> "$GITHUB_STEP_SUMMARY"
# Example: Conditional step
- name: 'Optional feature'
if: inputs.enable_feature == 'true'
shell: bash
run: |
# Optional feature
echo "Feature enabled ⚡"
# Example: Using external actions (always pin to SHA)
- name: 'Use external action'
# yamllint disable-line rule:line-length
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
path: 'checkout-path'
```
### Key Patterns for action.yaml
1. **Always use SPDX headers** at the top
2. **Use emojis** in action names for visual distinction (🐍 🔨 ☸️ 🏷️ etc.)
3. **Comment type hints** for inputs (GitHub doesn't use them but they help documentation)
4. **Pin external actions to SHA** with version comment
5. **Use `shell: bash`** for all composite action steps
6. **Leverage `$GITHUB_STEP_SUMMARY`** for user-friendly output
7. **Set `$GITHUB_OUTPUT`** for outputs, `$GITHUB_ENV`** for env vars
8. **Use yamllint disable comments** for long lines
9. **Validate all inputs** early in the action
10. **Use descriptive step names** that explain what's happening
---
## Licensing and Compliance
### Required Files
#### 1. LICENSE (Apache 2.0)
```
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
...
```
Download from: https://www.apache.org/licenses/LICENSE-2.0.txt
#### 2. LICENSES/ Directory
For REUSE compliance, create a `LICENSES/` directory:
```
LICENSES/
└── Apache-2.0.txt
```
#### 3. REUSE.toml
```toml
version = 1
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
[[annotations]]
path = [
"docs/**",
"tests/**"
]
SPDX-License-Identifier = "Apache-2.0"
SPDX-FileCopyrightText = "2025 The Linux Foundation"
```
#### 4. SPDX Headers
Add to **every file** (including YAML, shell scripts, Python, Go, etc.):
```yaml
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
```
For code files:
```python
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
```
For markdown:
```markdown
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2025 The Linux Foundation
-->
```
---
## Code Quality and Linting
### .editorconfig
```ini
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{json,yaml,yml}]
indent_size = 2
[*.markdown]
max_line_length = 80
[*.py]
max_line_length = 120
[*.sh]
max_line_length = 80
```
### .yamllint
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
extends: default
rules:
empty-lines:
max-end: 1
comments:
min-spaces-from-content: 1
level: error
line-length:
max: 120
level: warning
```
### .gitlint
```ini
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
[general]
contrib=contrib-title-conventional-commits,contrib-body-requires-signed-off-by
[contrib-title-conventional-commits]
types=Fix,Feat,Chore,Docs,Style,Refactor,Perf,Test,Revert,CI,Build
[ignore-body-lines]
regex=(.*)https?://(.*)
```
### .pre-commit-config.yaml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
ci:
autofix_commit_msg: |
Chore: pre-commit autofixes
Signed-off-by: pre-commit-ci[bot] <[email protected]>
autoupdate_commit_msg: |
Chore: pre-commit autoupdate
Signed-off-by: pre-commit-ci[bot] <[email protected]>
exclude: "^docs/conf.py"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-added-large-files
- id: check-ast
- id: check-json
- id: check-merge-conflict
- id: check-xml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: requirements-txt-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
- id: no-commit-to-branch
args:
- --branch=dev
- --branch=master
- --branch=main
- --branch=rc
- --branch=production
- repo: https://github.com/jorisroovers/gitlint
rev: acc9d9de6369b76d22cb4167029d2035e8730b98 # frozen: v0.19.1
hooks:
- id: gitlint
- repo: https://github.com/adrienverge/yamllint.git
rev: 79a6b2b1392eaf49cdd32ac4f14be1a809bbd8f7 # frozen: v1.37.1
hooks:
- id: yamllint
types: [yaml]
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1
hooks:
- id: shellcheck
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: 192ad822316c3a22fb3d3cc8aa6eafa0b8488360 # frozen: v0.45.0
hooks:
- id: markdownlint
args: ["--fix"]
- repo: https://github.com/fsfe/reuse-tool
rev: 9245cd26448e246cda84f70711cc9d5f970855d6 # frozen: v6.1.2
hooks:
- id: reuse
- repo: https://github.com/Mateusz-Grzelinski/actionlint-py
rev: 2773c7d04be5d0ceb075f215b6d0a4eacd4a432b # frozen: v1.7.8.24
hooks:
- id: actionlint
- repo: https://github.com/codespell-project/codespell
rev: 63c8f8312b7559622c0d82815639671ae42132ac # frozen: v2.4.1
hooks:
- id: codespell
args: ["--ignore-words=.codespell"]
- repo: https://github.com/python-jsonschema/check-jsonschema.git
rev: 83b816d020105076daac266dbf6bfed199a2da93 # frozen: 0.34.1
hooks:
- id: check-github-actions
- id: check-github-workflows
- id: check-jsonschema
name: Check GitHub Workflows set timeout-minutes
args:
- --builtin-schema
- github-workflows-require-timeout
files: ^\.github/workflows/[^/]+$
types:
- yaml
- id: check-readthedocs
# Python-specific (if applicable)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: f9351c924055bf6c7b4a4670237d3ce141e9f57c # frozen: v0.14.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 9f70dc58c23dfcca1b97af99eaeee3140a807c7e # frozen: v1.18.2
hooks:
- id: mypy
```
### .codespell
Create a file with words to ignore (one per line):
```
# Common technical terms to ignore
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
coordinador
aci
```
---
## Documentation
### README.md Structure
```markdown
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2025 The Linux Foundation
-->
# 🎯 Your Action Name
Brief description of what your action does.
## Features
- Feature 1
- Feature 2
- Feature 3
## Usage Example
<!-- markdownlint-disable MD046 -->
```yaml
- name: 'Use your action'
uses: org/your-action@main
with:
required_input: 'value'
optional_input: 'value'
```
<!-- markdownlint-enable MD046 -->
## Inputs
<!-- markdownlint-disable MD013 -->
| Variable Name | Required | Default | Description |
| --------------- | -------- | ------- | ---------------------------------- |
| required_input | True | | Description of required input |
| optional_input | False | 'value' | Description of optional input |
<!-- markdownlint-enable MD013 -->
## Outputs
<!-- markdownlint-disable MD013 -->
| Variable Name | Description |
| ------------- | ------------------------- |
| result | Description of output |
| count | Number of items processed |
<!-- markdownlint-enable MD013 -->
## How It Works
Detailed explanation of the action's behavior.
## Examples
### Basic Usage
```yaml
- uses: org/[email protected]
with:
required_input: 'value'
```
### Advanced Usage
```yaml
- uses: org/[email protected]
with:
required_input: 'value'
optional_input: 'custom'
enable_feature: 'true'
```
## Requirements
- List any prerequisites
- System requirements
- Tool versions
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
## License
Apache 2.0 - See [LICENSE](LICENSE) for details.
```
### .readthedocs.yml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
version: 2
sphinx:
configuration: docs/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: docs/requirements.txt
```
---
## Testing and CI/CD
### .github/workflows/testing.yaml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: "Test GitHub Action 🧪"
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
tests:
name: "Test local GitHub Action"
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 10
steps:
# Harden the runner
# yamllint disable-line rule:line-length
- uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: "Checkout repository"
# yamllint disable-line rule:line-length
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Test the action
- name: "Run action: ${{ github.repository }}"
uses: ./
with:
required_input: "test-value"
optional_input: "custom-value"
# Validate outputs
- name: "Validate action outputs"
shell: bash
run: |
# Validate action outputs
echo "Add validation logic here"
```
### .github/workflows/tag-push.yaml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: 'Release on Tag Push 🚀'
# yamllint disable-line rule:truthy
on:
push:
tags:
- '**'
permissions: {}
jobs:
promote-release:
name: 'Promote Draft Release'
if: github.ref_type == 'tag'
runs-on: 'ubuntu-latest'
permissions:
contents: write
timeout-minutes: 3
steps:
# yamllint disable-line rule:line-length
- uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
# yamllint disable-line rule:line-length
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: 'Verify Pushed Tag'
# yamllint disable-line rule:line-length
uses: lfreleng-actions/tag-push-verify-action@80e2bdbbb9ee7b67557a31705892b75e75d2859e # v0.1.1
with:
versioning: 'semver'
- name: 'Promote draft release'
# yamllint disable-line rule:line-length
uses: lfreleng-actions/draft-release-promote-action@d7e7df12e32fa26b28dbc2f18a12766482785399 # v0.1.2
with:
token: "${{ secrets.GITHUB_TOKEN }}"
tag: "${{ github.ref_name }}"
latest: true
```
### .github/workflows/semantic-pull-request.yaml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: '🛠️ Semantic Pull Request'
# yamllint disable-line rule:truthy
on:
pull_request:
types: [opened, reopened, edited, synchronize]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
semantic-pull-request:
name: "Semantic Pull Request"
permissions:
contents: read
runs-on: ubuntu-24.04
timeout-minutes: 3
steps:
# yamllint disable-line rule:line-length
- uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: "Validate pull request title"
# yamllint disable-line rule:line-length
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
Fix
Feat
Chore
Docs
Style
Refactor
Perf
Test
Revert
CI
Build
validateSingleCommit: true
validateSingleCommitMatchesPrTitle: true
```
### .github/workflows/sha-pinned-actions.yaml
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: '📌 Audit GitHub Actions'
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
pull_request:
branches:
- main
paths: ['.github/**']
permissions: {}
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
jobs:
verify:
name: 'Check SHA pinned actions'
# yamllint disable-line rule:line-length
uses: lfit/releng-reusable-workflows/.github/workflows/reuse-verify-github-actions.yaml@353a06e737c950cb297b3138784729ebd24e4861 # v0.2.22
permissions:
contents: read
```
### .github/dependabot.yml
```yaml
---
# SPDX-FileCopyrightText: 2025 The Linux Foundation
# SPDX-License-Identifier: Apache-2.0
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "Chore"
```
### .github/release-drafter.yml
```yaml
---
# SPDX-FileCopyrightText: 2025 The Linux Foundation
# SPDX-License-Identifier: Apache-2.0
name-template: "v$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
sort-direction: ascending
categories:
- title: ":boom: Breaking Change :boom:"
labels:
- "breaking-change"
- title: ":zap: Enhancements :zap:"
labels:
- "enhancement"
- title: ":sparkles: New Features :sparkles:"
labels:
- "feature"
- title: ":bug: Bug Fixes :bug:"
labels:
- "fix"
- "bugfix"
- "bug"
- title: ":wrench: Maintenance :wrench:"
labels:
- "chore"
- "documentation"
- "maintenance"
- "dependencies"
- title: ":mortar_board: Code Quality :mortar_board:"
labels:
- "code-quality"
- "CI"
- "test"
autolabeler:
- label: "feature"
title:
- "/feat:/i"
- label: "bug"
title:
- "/fix:/i"
- label: "chore"
title:
- "/chore:/i"
- label: "documentation"
title:
- "/docs:/i"
template: |
$CHANGES
## Links
- [Submit bugs/feature requests](https://github.com/$OWNER/$REPOSITORY/issues)
```
---
## Implementation Patterns
### Pattern 1: Composite Action (Shell-based)
**Best for:** Simple logic, file operations, CLI tools
```yaml
runs:
using: 'composite'
steps:
- name: 'Execute logic'
shell: bash
env:
INPUT_VALUE: ${{ inputs.value }}
run: |
# Your bash script here
value="${INPUT_VALUE}"
echo "result=success" >> "$GITHUB_OUTPUT"
```
### Pattern 2: Go-based Action
**Best for:** Complex logic, performance-critical operations, binary distribution
**Structure:**
```
your-action/
├── cmd/
│ └── your-tool/
│ └── main.go
├── pkg/
│ └── logic/
│ └── logic.go
├── go.mod
├── go.sum
└── action.yaml
```
**action.yaml approach:**
```yaml
runs:
using: 'composite'
steps:
- name: 'Build and execute'
shell: bash
working-directory: ${{ github.action_path }}
run: |
# Determine architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case $ARCH in
x86_64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
esac
BINARY_NAME="tool-${OS}-${ARCH}"
# Build binary
CGO_ENABLED=0 GOOS="${OS}" GOARCH="${ARCH}" go build \
-ldflags="-s -w" \
-o "${BINARY_NAME}" \
./cmd/your-tool
chmod +x "${BINARY_NAME}"
# Execute
./"${BINARY_NAME}" --flag="${{ inputs.value }}"
# Cleanup
rm -f "${BINARY_NAME}"
```
### Pattern 3: Python-based Action
**Best for:** Data processing, API interactions, complex workflows
**Structure:**
```
your-action/
├── src/
│ └── your_package/
│ ├── __init__.py
│ ├── core.py
│ └── cli.py
├── pyproject.toml
└── action.yaml
```
**pyproject.toml:**
```toml
[project]
name = "your-action"
version = "1.0.0"
requires-python = ">=3.9"
dependencies = [
"requests>=2.31.0",
]
[project.scripts]
your-tool = "your_package.cli:main"
```
**action.yaml approach:**
```yaml
runs:
using: 'composite'
steps:
- name: 'Setup Python'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: 'Install and execute'
shell: bash
working-directory: ${{ github.action_path }}
run: |
pip install -q .
your-tool --input="${{ inputs.value }}"
```
### Pattern 4: Docker-based Operations
**Best for:** Docker image manipulation, container operations
```yaml
runs:
using: 'composite'
steps:
- name: 'Docker operations'
shell: bash
run: |
# Save docker images
docker save image1 image2 -o images.tar
- name: 'Upload artifacts'
uses: actions/upload-artifact@v4
with:
name: docker-images
path: images.tar
retention-days: 1
```
### Pattern 5: Caching Pattern
**Best for:** Actions that install dependencies
```yaml
- name: 'Cache dependencies'
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.cache/pypoetry
.venv
key: >-
python-${{ runner.os }}-${{ steps.setup.outputs.python-version }}-
${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
restore-keys: |
python-${{ runner.os }}-${{ steps.setup.outputs.python-version }}-
python-${{ runner.os }}-
```
### Pattern 6: Input Validation
```yaml
- name: 'Validate inputs'
shell: bash
run: |
# Validate inputs
required_inputs=("input1" "input2")
for input in "${required_inputs[@]}"; do
case "$input" in
"input1")
if [ -z "${{ inputs.input1 }}" ]; then
echo 'Error: input1 is required ❌'
exit 1
fi
;;
"input2")
if [ ! -d "${{ inputs.input2 }}" ]; then
echo 'Error: input2 must be valid directory ❌'
exit 1
fi
;;
esac
done
echo 'All inputs validated ✅'
```
### Pattern 7: Error Handling and Cleanup
```yaml
- name: 'Main operation'
shell: bash
run: |
set -euo pipefail
# Create temp resources
temp_file=$(mktemp)
# Trap to ensure cleanup
cleanup() {
rm -f "$temp_file"
}
trap cleanup EXIT
# Your logic here
if ! perform_operation; then
echo "Operation failed ❌" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "Operation succeeded ✅" >> "$GITHUB_STEP_SUMMARY"
```
### Pattern 8: Multi-platform Binary Distribution
```yaml
- name: 'Download or build binary'
shell: bash
run: |
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case $ARCH in
x86_64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) echo "Unsupported: $ARCH" >&2; exit 1 ;;
esac
BINARY_NAME="tool-${OS}-${ARCH}"
if [ -n "${{ inputs.download_url }}" ]; then
# Download pre-built binary
curl -fsSL -o "${BINARY_NAME}" "${{ inputs.download_url }}"
# Verify checksum
if [ -n "${{ inputs.checksum }}" ]; then
ACTUAL=$(sha256sum "${BINARY_NAME}" | cut -d' ' -f1)
if [ "${ACTUAL}" != "${{ inputs.checksum }}" ]; then
echo "Checksum mismatch ❌"
exit 1
fi
fi
else
# Build locally
go build -o "${BINARY_NAME}" ./cmd/tool
fi
chmod +x "${BINARY_NAME}"
```
---
## Security Best Practices
### 1. Always Pin Actions to SHA
❌ **Bad:**
```yaml
uses: actions/checkout@v5
```
✅ **Good:**
```yaml
# yamllint disable-line rule:line-length
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
```
### 2. Minimal Permissions
```yaml
permissions: {} # Default to no permissions
jobs:
job-name:
permissions:
contents: read # Only what's needed
```
### 3. Harden Runners
```yaml
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
```
### 4. Secure Secret Handling
```yaml
# Never log secrets
- name: 'Use secret'
shell: bash
env:
SECRET_VALUE: ${{ secrets.SECRET }}
run: |
# Use $SECRET_VALUE without echo
command --token="$SECRET_VALUE"
```
### 5. Input Sanitization
```yaml
- name: 'Sanitize inputs'
shell: bash
run: |
# Validate format
if ! echo "${{ inputs.version }}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo 'Invalid version format ❌'
exit 1
fi
```
### 6. Timeout Protection
```yaml
jobs:
job-name:
timeout-minutes: 10 # Always set timeout
```
### 7. Dependency Security
- Use Dependabot for automated updates
- Scan for vulnerabilities with `pip-audit`, `npm audit`, etc.
- Pin dependencies to specific versions
---
## Versioning and Releases
### Semantic Versioning
Follow [SemVer](https://semver.org/):
- **MAJOR.MINOR.PATCH** (e.g., `v1.2.3`)
- Prefix with `v`
- Use pre-release tags: `v1.0.0-beta.1`, `v1.0.0-rc.1`
### Git Commit Convention
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>: <description>
[optional body]
[optional footer]
Signed-off-by: Your Name <[email protected]>
```
**Types:**
- `Feat:` - New feature
- `Fix:` - Bug fix
- `Chore:` - Maintenance task
- `Docs:` - Documentation
- `Style:` - Code style changes
- `Refactor:` - Code refactoring
- `Perf:` - Performance improvement
- `Test:` - Tests
- `CI:` - CI/CD changes
- `Build:` - Build system changes
### Release Process
1. **Development** - Work on `main` branch
2. **Create PR** - Semantic title, get reviewed
3. **Merge** - Auto-generates draft release
4. **Tag** - Create and push tag: `git tag v1.0.0 && git push origin v1.0.0`
5. **Auto-publish** - Tag push triggers release promotion
---
## Quick Start Template
### Minimal Working Action
**Directory structure:**
```
my-simple-action/
├── .github/
│ ├── dependabot.yml
│ ├── release-drafter.yml
│ └── workflows/
│ ├── testing.yaml
│ └── tag-push.yaml
├── .editorconfig
├── .gitignore
├── .gitlint
├── .pre-commit-config.yaml
├── .yamllint
├── action.yaml
├── LICENSE
├── LICENSES/
│ └── Apache-2.0.txt
├── README.md
└── REUSE.toml
```
**action.yaml:**
```yaml
---
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
name: 'My Simple Action 🎯'
description: 'Does something useful'
inputs:
input_value:
description: 'Input value to process'
required: true
outputs:
result:
description: 'Processing result'
value: ${{ steps.process.outputs.result }}
runs:
using: 'composite'
steps:
- name: 'Process input'
id: process
shell: bash
env:
INPUT_VALUE: ${{ inputs.input_value }}
run: |
# Process input
echo "# 🎯 My Simple Action" >> "$GITHUB_STEP_SUMMARY"
result="Processed: ${INPUT_VALUE}"
echo "result=$result" >> "$GITHUB_OUTPUT"
echo "Result: $result ✅" >> "$GITHUB_STEP_SUMMARY"
```
**.gitignore:**
```gitignore
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 The Linux Foundation
# Python
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
*.egg-info/
dist/
build/
# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/
vendor/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
.coverage
htmlcov/
.pytest_cache/
.tox/
```
---
## Tools and Dependencies Summary
### Essential Tools
1. **REUSE Tool** - License compliance
```bash
pip install reuse
reuse lint
```
2. **pre-commit** - Git hooks
```bash
pip install pre-commit
pre-commit install
pre-commit run --all-files
```
3. **yamllint** - YAML linting
```bash
pip install yamllint
yamllint .
```
4. **actionlint** - GitHub Actions linting
```bash
# Via pre-commit or:
go install github.com/rhysd/actionlint/cmd/actionlint@latest
actionlint
```
5. **shellcheck** - Shell script linting
```bash
apt install shellcheck # or via pre-commit
shellcheck script.sh
```
6. **markdownlint** - Markdown linting
```bash
npm install -g markdownlint-cli
markdownlint '**/*.md'
```
### Language-Specific Tools
**Python:**
- `ruff` - Fast Python linter/formatter
- `mypy` - Static type checker
- `pytest` - Testing framework
- `pip-audit` - Security scanning
**Go:**
- `golangci-lint` - Linter aggregator
- `go test` - Testing
- `go vet` - Static analysis
**Shell:**
- `shellcheck` - Shell script analysis
- `shfmt` - Shell formatter
### CI/CD Services
1. **pre-commit.ci** - Automated pre-commit hooks
2. **Dependabot** - Dependency updates
3. **ReadTheDocs** - Documentation hosting
4. **GitHub Actions** - CI/CD platform
---
## Common Patterns Reference
### Boolean Input Handling
```yaml
# Define as string with default
inputs:
enable_feature:
required: false
default: 'false'
# Check in bash
- shell: bash
run: |
if [ "${{ inputs.enable_feature }}" = "true" ]; then
echo "Feature enabled"
fi
```
### Path Handling
```yaml
- shell: bash
run: |
path_prefix="${{ inputs.path_prefix }}"
# Handle empty/default
if [ -z "$path_prefix" ]; then
path_prefix="."
fi
# Strip trailing slash
path_prefix="${path_prefix%/}"
# Validate
if [ ! -d "$path_prefix" ]; then
echo "Invalid path ❌"
exit 1
fi
```
### Array/List Inputs
```yaml
- shell: bash
run: |
# Space-separated list
items="${{ inputs.items }}"
read -ra item_array <<< "$items"
for item in "${item_array[@]}"; do
echo "Processing: $item"
done
```
### Conditional Execution
```yaml
# In action.yaml
- name: 'Conditional step'
if: inputs.condition == 'true' && github.ref_type == 'tag'
shell: bash
run: |
echo "Running conditional logic"
```
### Environment Variable Propagation
```yaml
- name: 'Set variables'
shell: bash
run: |
echo "VAR_NAME=value" >> "$GITHUB_ENV"
- name: 'Use variables'
shell: bash
run: |
echo "Using: $VAR_NAME"
```
### Output Multiple Values
```yaml
- name: 'Set multiple outputs'
id: outputs
shell: bash
run: |
echo "output1=value1" >> "$GITHUB_OUTPUT"
echo "output2=value2" >> "$GITHUB_OUTPUT"
echo "count=42" >> "$GITHUB_OUTPUT"
```
### Working with JSON
```yaml
- shell: bash
run: |
# Parse JSON input
json='${{ inputs.json_input }}'
value=$(echo "$json" | jq -r '.key')
# Create JSON output
json_output=$(jq -n \
--arg key1 "value1" \
--arg key2 "value2" \
'{key1: $key1, key2: $key2}')
echo "json_output=$json_output" >> "$GITHUB_OUTPUT"
```
---
## Best Practices Checklist
### Before First Commit
- [ ] Add SPDX headers to all files
- [ ] Configure `.editorconfig`
- [ ] Set up `.pre-commit-config.yaml`
- [ ] Add `.gitignore`
- [ ] Create LICENSE file
- [ ] Set up REUSE compliance
- [ ] Configure linters (yamllint, shellcheck, etc.)
- [ ] Write initial README.md
### Action Development
- [ ] Define clear inputs and outputs
- [ ] Validate all inputs early
- [ ] Use descriptive step names
- [ ] Add comments to complex bash logic
- [ ] Pin all external actions to SHA
- [ ] Set timeouts on all jobs
- [ ] Use minimal permissions
- [ ] Add user-friendly output to `$GITHUB_STEP_SUMMARY`
- [ ] Handle errors gracefully
- [ ] Clean up temporary resources
### Testing
- [ ] Create testing workflow
- [ ] Test with valid inputs
- [ ] Test with invalid inputs
- [ ] Test edge cases
- [ ] Test on ubuntu-24.04
- [ ] Document test scenarios
### Documentation
- [ ] Complete README with examples
- [ ] Document all inputs/outputs
- [ ] Add usage examples
- [ ] Include troubleshooting section
- [ ] Set up ReadTheDocs (if complex)
### CI/CD
- [ ] Set up Dependabot
- [ ] Configure release-drafter
- [ ] Add semantic PR checks
- [ ] Create tag-push workflow
- [ ] Enable branch protection
- [ ] Require PR reviews
- [ ] Require status checks to pass
### Security
- [ ] Run security scanning
- [ ] Use harden-runner
- [ ] Audit dependencies
- [ ] No secrets in logs
- [ ] Validate all external inputs
- [ ] Use least privilege permissions
### Release
- [ ] Follow semantic versioning
- [ ] Tag releases properly (v1.0.0)
- [ ] Generate release notes
- [ ] Update documentation
- [ ] Test release process
---
## Additional Resources
### Official Documentation
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Composite Actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action)
- [Action Metadata Syntax](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions)
### Tools
- [REUSE Tool](https://reuse.software/)
- [pre-commit](https://pre-commit.com/)
- [Conventional Commits](https://www.conventionalcommits.org/)
- [Semantic Versioning](https://semver.org/)
### Security
- [OpenSSF Scorecard](https://github.com/ossf/scorecard)
- [StepSecurity Harden Runner](https://github.com/step-security/harden-runner)
- [GitHub Security Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
---
## Conclusion
This MVP provides a comprehensive foundation for creating production-quality GitHub Actions based on patterns observed across 43+ real-world actions. The key principles are:
1. **Consistency** - Follow established patterns and conventions
2. **Security** - Pin dependencies, validate inputs, use minimal permissions
3. **Quality** - Automated linting, testing, and compliance checks
4. **Documentation** - Clear, comprehensive documentation with examples
5. **Maintainability** - Automated updates, semantic versioning, clean code
Start with the Quick Start Template and progressively add features based on your action's complexity and requirements. The patterns and configurations provided here are battle-tested in production environments and represent current best practices in the GitHub Actions ecosystem.
---
**Document Version:** 1.0.0
**Last Updated:** 2025
**Based on Analysis of:** 43 GitHub Actions from lfit/lfreleng-actions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment