Last active
October 25, 2025 01:01
-
-
Save askb/4b69882938f9ce8df7aab4319dc1c5b5 to your computer and use it in GitHub Desktop.
Generic MVP for creating new lfreleng-actions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| 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: | |
| - 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