38 KiB
Reusable Workflow Templates for Centralized Repositories
This document contains example templates for reusable workflows that will be distributed across two centralized repositories based on sensitivity.
Dual Repository Architecture
MokoStandards (Public Repository)
- Purpose: Public, community-accessible workflows and standards
- Content: Quality checks, testing workflows, public CI/CD templates
- Visibility: Public (open source)
- Target Audience: Internal teams and external community
.github-private (Private Repository)
- Purpose: Sensitive, proprietary workflows and deployment logic
- Content: Deployment workflows, release pipelines, credential handling
- Visibility: Private (organization only)
- Target Audience: Internal teams only
Repository Structures
MokoStandards (Public):
MokoStandards/
├── .github/
│ └── workflows/
│ ├── reusable-php-quality.yml
│ ├── reusable-joomla-testing.yml
│ ├── reusable-dolibarr-testing.yml
│ └── reusable-security-scan.yml
├── scripts/
│ └── shared/
│ ├── extension_utils.py
│ └── common.py
└── docs/
├── STANDARDS.md
└── USAGE.md
.github-private (Private):
.github-private/
├── .github/
│ └── workflows/
│ ├── reusable-release-pipeline.yml
│ ├── reusable-deploy-staging.yml
│ └── reusable-deploy-production.yml
├── scripts/
│ └── deployment/
└. docs/
└── USAGE.md
Usage in Main Repositories
Basic Patterns
Pattern 1: Calling Public Workflow from MokoStandards
name: Quality Checks
on:
push:
branches: [ main ]
jobs:
quality:
uses: mokoconsulting-tech/MokoStandards/.github/workflows/reusable-php-quality.yml@v1
with:
php-versions: '["8.0", "8.1", "8.2", "8.3"]'
secrets: inherit
Pattern 2: Calling Private Workflow from .github-private
name: Deploy
on:
workflow_dispatch:
jobs:
deploy:
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-deploy.yml@main
with:
environment: staging
secrets: inherit
Pattern 3: Combining Both (Public Quality + Private Deployment)
name: CI/CD Pipeline
on:
push:
branches: [ main ]
jobs:
quality:
uses: mokoconsulting-tech/MokoStandards/.github/workflows/reusable-php-quality.yml@v1
with:
php-versions: '["8.0", "8.1", "8.2", "8.3"]'
secrets: inherit
deploy:
needs: quality
if: success()
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-deploy.yml@main
with:
environment: staging
secrets: inherit
Complete Workflow Examples by Repository
Example 1: PHP Quality Check (MokoStandards - Public)
In main repository (.github/workflows/php_quality.yml):
name: PHP Code Quality
on:
pull_request:
branches: [ main, dev/*, rc/* ]
push:
branches: [ main, dev/*, rc/* ]
permissions:
contents: read
jobs:
php-quality:
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-php-quality.yml@main
with:
php-versions: '["8.0", "8.1", "8.2", "8.3"]'
php-extensions: 'mbstring, xml, curl, zip'
working-directory: '.'
secrets: inherit
Example: Release Pipeline
In main repository (.github/workflows/release.yml):
name: Release Pipeline
on:
workflow_dispatch:
inputs:
release_classification:
description: 'Release classification'
required: true
default: 'auto'
type: choice
options:
- auto
- rc
- stable
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-release-pipeline.yml@main
with:
release_classification: ${{ inputs.release_classification }}
platform: 'joomla' # or 'dolibarr'
secrets: inherit
Reusable Workflow Template Examples
Template: reusable-php-quality.yml
name: Reusable PHP Quality Workflow
on:
workflow_call:
inputs:
php-versions:
description: 'JSON array of PHP versions to test'
required: false
type: string
default: '["8.0", "8.1", "8.2", "8.3"]'
php-extensions:
description: 'PHP extensions to install'
required: false
type: string
default: 'mbstring, xml, curl, zip'
working-directory:
description: 'Working directory'
required: false
type: string
default: '.'
permissions:
contents: read
jobs:
compatibility-check:
name: PHP Compatibility Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: ${{ inputs.php-extensions }}
- name: Install dependencies
run: |
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
composer global require "phpcompatibility/php-compatibility:^9.0" --with-all-dependencies
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
- name: Check PHP 8.0+ Compatibility
working-directory: ${{ inputs.working-directory }}
run: phpcs --standard=PHPCompatibility --runtime-set testVersion 8.0- src/
phpcs:
name: PHP_CodeSniffer
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ${{ fromJson(inputs.php-versions) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ inputs.php-extensions }}
- name: Install PHP_CodeSniffer
run: composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
- name: Run PHP_CodeSniffer
working-directory: ${{ inputs.working-directory }}
run: phpcs --standard=phpcs.xml src/
phpstan:
name: PHPStan Static Analysis
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ${{ fromJson(inputs.php-versions) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: ${{ inputs.php-extensions }}
- name: Install PHPStan
run: |
composer global require phpstan/phpstan "^1.0" --with-all-dependencies
- name: Run PHPStan
working-directory: ${{ inputs.working-directory }}
run: phpstan analyse --configuration=phpstan.neon
Template: reusable-release-pipeline.yml
name: Reusable Release Pipeline
on:
workflow_call:
inputs:
release_classification:
description: 'Release classification (auto, rc, stable)'
required: false
type: string
default: 'auto'
platform:
description: 'Extension platform (joomla, dolibarr)'
required: false
type: string
default: 'joomla'
secrets:
FTP_SERVER:
required: true
FTP_USER:
required: true
FTP_KEY:
required: false
FTP_PASSWORD:
required: false
FTP_PATH:
required: true
FTP_PROTOCOL:
required: false
FTP_PORT:
required: false
permissions:
contents: write
id-token: write
attestations: write
jobs:
guard:
name: Guardrails and Metadata
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
# ... other outputs
steps:
# Guard logic here
build-and-release:
name: Build and Release
needs: guard
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect Platform
id: platform
run: |
python3 scripts/release/detect_platform.py src
- name: Build ZIP
run: |
# Build logic here
# ... remaining steps
Benefits of Centralized Workflows
- Single Source of Truth: Update workflow logic in one place
- Version Control: Pin to specific versions (@v1, @main, @sha)
- Testing: Test changes in .github-private before rolling out
- Security: Keep sensitive logic private
- Reusability: Use same workflow across multiple repositories
Migration Checklist
- Create .github-private repository
- Set up repository permissions and protection rules
- Move workflow files and convert to reusable format
- Update main repository workflows to call reusable workflows
- Configure secrets at organization level
- Test all workflows end-to-end
- Update documentation
- Train team on new workflow structure
Advanced Workflow Patterns
Pattern 1: Multi-Stage Workflow with Approvals
Scenario: Deploy to staging automatically, but require approval for production.
Caller Workflow (.github/workflows/deploy.yml):
name: Deploy Application
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
type: choice
options:
- staging
- production
jobs:
deploy-staging:
if: ${{ inputs.environment == 'staging' }}
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-deploy.yml@v1
with:
environment: staging
deploy-url: https://staging.example.com
health-check-enabled: true
secrets: inherit
deploy-production:
if: ${{ inputs.environment == 'production' }}
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-deploy.yml@v1
with:
environment: production
deploy-url: https://production.example.com
health-check-enabled: true
require-approval: true
secrets: inherit
Reusable Workflow (.github-private/.github/workflows/reusable-deploy.yml):
name: Reusable Deployment Workflow
on:
workflow_call:
inputs:
environment:
required: true
type: string
deploy-url:
required: true
type: string
health-check-enabled:
required: false
type: boolean
default: true
require-approval:
required: false
type: boolean
default: false
secrets:
FTP_SERVER:
required: true
FTP_USER:
required: true
FTP_PASSWORD:
required: true
FTP_PATH:
required: true
outputs:
deployment-id:
description: 'Unique deployment identifier'
value: ${{ jobs.deploy.outputs.deployment-id }}
deployment-url:
description: 'URL where application was deployed'
value: ${{ inputs.deploy-url }}
permissions:
contents: read
deployments: write
jobs:
approval:
name: Deployment Approval
runs-on: ubuntu-latest
if: ${{ inputs.require-approval }}
environment:
name: ${{ inputs.environment }}-approval
steps:
- name: Wait for Approval
run: echo "Deployment to ${{ inputs.environment }} approved"
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-latest
needs: [approval]
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
outputs:
deployment-id: ${{ steps.create-deployment.outputs.deployment-id }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create GitHub Deployment
id: create-deployment
uses: actions/github-script@v7
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: '${{ inputs.environment }}',
auto_merge: false,
required_contexts: []
});
core.setOutput('deployment-id', deployment.data.id);
return deployment.data.id;
- name: Update Deployment Status (In Progress)
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: ${{ steps.create-deployment.outputs.deployment-id }},
state: 'in_progress',
description: 'Deployment in progress'
});
- name: Deploy via SFTP
run: |
# Install lftp
sudo apt-get update && sudo apt-get install -y lftp
# Create deployment package
tar -czf deployment.tar.gz src/
# Upload via SFTP
lftp -c "
set sftp:auto-confirm yes;
open sftp://${{ secrets.FTP_USER }}:${{ secrets.FTP_PASSWORD }}@${{ secrets.FTP_SERVER }};
cd ${{ secrets.FTP_PATH }};
put deployment.tar.gz;
quit
"
- name: Health Check
if: ${{ inputs.health-check-enabled }}
run: |
echo "Performing health check on ${{ inputs.deploy-url }}"
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -f -s "${{ inputs.deploy-url }}/health" > /dev/null; then
echo "Health check passed!"
exit 0
fi
echo "Health check failed, retrying... ($((attempt+1))/$max_attempts)"
sleep 10
attempt=$((attempt+1))
done
echo "Health check failed after $max_attempts attempts"
exit 1
- name: Update Deployment Status (Success)
if: success()
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: ${{ steps.create-deployment.outputs.deployment-id }},
state: 'success',
description: 'Deployment successful',
environment_url: '${{ inputs.deploy-url }}'
});
- name: Update Deployment Status (Failure)
if: failure()
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: ${{ steps.create-deployment.outputs.deployment-id }},
state: 'failure',
description: 'Deployment failed'
});
Pattern 2: Dynamic Matrix from API/File
Scenario: Test against multiple versions dynamically fetched from external source.
Caller Workflow:
name: Dynamic Matrix Testing
on:
push:
branches: [main, develop]
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Fetch Supported Versions
id: set-matrix
run: |
# Fetch from API or read from file
MATRIX=$(curl -s https://api.example.com/supported-versions.json)
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
test:
needs: generate-matrix
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-test.yml@v1
with:
test-matrix: ${{ needs.generate-matrix.outputs.matrix }}
secrets: inherit
Reusable Workflow:
name: Reusable Test Workflow
on:
workflow_call:
inputs:
test-matrix:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(inputs.test-matrix) }}
steps:
- name: Test with ${{ matrix.php }} and ${{ matrix.framework }}
run: |
echo "Testing PHP ${{ matrix.php }} with ${{ matrix.framework }}"
Pattern 3: Workflow Chaining with Artifacts
Scenario: Build in one workflow, deploy in another, share artifacts.
Build Workflow (.github-private/.github/workflows/reusable-build.yml):
name: Reusable Build Workflow
on:
workflow_call:
inputs:
platform:
required: true
type: string
build-config:
required: false
type: string
default: 'production'
outputs:
artifact-name:
description: 'Name of the build artifact'
value: ${{ jobs.build.outputs.artifact-name }}
artifact-sha256:
description: 'SHA256 checksum of the artifact'
value: ${{ jobs.build.outputs.artifact-sha256 }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.package.outputs.artifact-name }}
artifact-sha256: ${{ steps.checksum.outputs.sha256 }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect Platform
id: platform
run: |
python3 scripts/release/detect_platform.py src
- name: Build Package
id: package
run: |
python3 scripts/release/package_extension.py dist
ARTIFACT_NAME=$(ls dist/*.zip | head -1)
echo "artifact-name=$(basename $ARTIFACT_NAME)" >> $GITHUB_OUTPUT
- name: Calculate Checksum
id: checksum
run: |
cd dist
SHA256=$(sha256sum *.zip | awk '{print $1}')
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
echo "$SHA256 $(ls *.zip)" > checksums.txt
- name: Upload Build Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.artifact-name }}
path: dist/*.zip
retention-days: 30
- name: Upload Checksums
uses: actions/upload-artifact@v4
with:
name: checksums
path: dist/checksums.txt
retention-days: 30
Deploy Workflow (.github-private/.github/workflows/reusable-deploy.yml):
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
artifact-name:
required: true
type: string
artifact-sha256:
required: true
type: string
environment:
required: true
type: string
secrets:
FTP_SERVER:
required: true
FTP_USER:
required: true
FTP_PASSWORD:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Download Build Artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: ./artifacts
- name: Verify Checksum
run: |
cd ./artifacts
ACTUAL_SHA256=$(sha256sum *.zip | awk '{print $1}')
EXPECTED_SHA256="${{ inputs.artifact-sha256 }}"
if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
echo "Checksum mismatch!"
echo "Expected: $EXPECTED_SHA256"
echo "Actual: $ACTUAL_SHA256"
exit 1
fi
echo "Checksum verified successfully"
- name: Deploy to ${{ inputs.environment }}
run: |
# Deploy logic here
echo "Deploying ${{ inputs.artifact-name }} to ${{ inputs.environment }}"
Caller Workflow (chaining build and deploy):
name: Build and Deploy
on:
workflow_dispatch:
inputs:
environment:
required: true
type: choice
options: [staging, production]
jobs:
build:
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-build.yml@v1
with:
platform: 'joomla'
build-config: 'production'
deploy:
needs: build
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-deploy.yml@v1
with:
artifact-name: ${{ needs.build.outputs.artifact-name }}
artifact-sha256: ${{ needs.build.outputs.artifact-sha256 }}
environment: ${{ inputs.environment }}
secrets: inherit
Pattern 4: Conditional Steps Based on Repository
Scenario: Different behavior for different repositories calling the same workflow.
Reusable Workflow:
name: Reusable CI Workflow
on:
workflow_call:
inputs:
repository-type:
description: 'Type of repository (template, component, plugin)'
required: false
type: string
default: 'auto-detect'
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect Repository Type
id: detect
run: |
if [ "${{ inputs.repository-type }}" == "auto-detect" ]; then
if [ -f "src/templates/templateDetails.xml" ]; then
echo "type=template" >> $GITHUB_OUTPUT
elif [ -f "src/component.xml" ]; then
echo "type=component" >> $GITHUB_OUTPUT
else
echo "type=plugin" >> $GITHUB_OUTPUT
fi
else
echo "type=${{ inputs.repository-type }}" >> $GITHUB_OUTPUT
fi
- name: Run Template-Specific Tests
if: ${{ steps.detect.outputs.type == 'template' }}
run: |
echo "Running template-specific tests"
# Template tests here
- name: Run Component-Specific Tests
if: ${{ steps.detect.outputs.type == 'component' }}
run: |
echo "Running component-specific tests"
# Component tests here
- name: Run Plugin-Specific Tests
if: ${{ steps.detect.outputs.type == 'plugin' }}
run: |
echo "Running plugin-specific tests"
# Plugin tests here
- name: Common Tests
run: |
echo "Running common tests for all types"
# Common tests here
Complete Workflow Implementation Examples
Example 1: Complete PHP Quality Workflow
File: .github-private/.github/workflows/reusable-php-quality.yml
name: Reusable PHP Quality Workflow
on:
workflow_call:
inputs:
php-versions:
description: 'JSON array of PHP versions'
required: false
type: string
default: '["8.0", "8.1", "8.2", "8.3"]'
source-directory:
description: 'Source directory to analyze'
required: false
type: string
default: 'src'
enable-cache:
description: 'Enable dependency caching'
required: false
type: boolean
default: true
fail-fast:
description: 'Stop all jobs if one fails'
required: false
type: boolean
default: false
outputs:
all-passed:
description: 'Whether all checks passed'
value: ${{ jobs.summary.outputs.all-passed }}
permissions:
contents: read
checks: write
jobs:
phpcs:
name: PHPCS (PHP ${{ matrix.php-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: ${{ inputs.fail-fast }}
matrix:
php-version: ${{ fromJson(inputs.php-versions) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, xml, curl, zip
coverage: none
tools: composer:v2
- name: Get Composer Cache Directory
id: composer-cache
if: ${{ inputs.enable-cache }}
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer Dependencies
if: ${{ inputs.enable-cache }}
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Dependencies
run: |
composer global require "squizlabs/php_codesniffer:^3.0" --with-all-dependencies
echo "$(composer config -g home)/vendor/bin" >> $GITHUB_PATH
- name: Run PHPCS
run: |
phpcs --version
phpcs --standard=phpcs.xml \
--report=full \
--report=checkstyle:phpcs-checkstyle.xml \
${{ inputs.source-directory }}/
- name: Annotate Code
if: always()
uses: staabm/annotate-pull-request-from-checkstyle@v1
with:
files: phpcs-checkstyle.xml
notices-as-warnings: true
phpstan:
name: PHPStan (PHP ${{ matrix.php-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: ${{ inputs.fail-fast }}
matrix:
php-version: ${{ fromJson(inputs.php-versions) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, xml, curl, zip
coverage: none
- name: Install Dependencies
run: |
composer global require "phpstan/phpstan:^1.0" --with-all-dependencies
echo "$(composer config -g home)/vendor/bin" >> $GITHUB_PATH
- name: Run PHPStan
run: |
phpstan --version
phpstan analyse \
--configuration=phpstan.neon \
--error-format=github \
--no-progress \
${{ inputs.source-directory }}/
summary:
name: Quality Check Summary
runs-on: ubuntu-latest
needs: [phpcs, phpstan]
if: always()
outputs:
all-passed: ${{ steps.check.outputs.all-passed }}
steps:
- name: Check Results
id: check
run: |
PHPCS_RESULT="${{ needs.phpcs.result }}"
PHPSTAN_RESULT="${{ needs.phpstan.result }}"
if [ "$PHPCS_RESULT" == "success" ] && [ "$PHPSTAN_RESULT" == "success" ]; then
echo "all-passed=true" >> $GITHUB_OUTPUT
echo "✅ All quality checks passed"
else
echo "all-passed=false" >> $GITHUB_OUTPUT
echo "❌ Some quality checks failed"
echo "PHPCS: $PHPCS_RESULT"
echo "PHPStan: $PHPSTAN_RESULT"
exit 1
fi
Example 2: Complete Release Pipeline Workflow
File: .github-private/.github/workflows/reusable-release-pipeline.yml
name: Reusable Release Pipeline
on:
workflow_call:
inputs:
release-classification:
description: 'Release classification'
required: false
type: string
default: 'auto'
platform:
description: 'Extension platform'
required: false
type: string
default: 'auto-detect'
skip-tests:
description: 'Skip test execution'
required: false
type: boolean
default: false
secrets:
FTP_SERVER:
required: true
FTP_USER:
required: true
FTP_PASSWORD:
required: true
FTP_PATH:
required: true
GPG_PRIVATE_KEY:
required: false
GPG_PASSPHRASE:
required: false
outputs:
version:
description: 'Released version'
value: ${{ jobs.metadata.outputs.version }}
download-url:
description: 'Download URL for release'
value: ${{ jobs.release.outputs.download-url }}
permissions:
contents: write
id-token: write
attestations: write
jobs:
metadata:
name: Extract Metadata
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
platform: ${{ steps.platform.outputs.platform }}
changelog: ${{ steps.changelog.outputs.content }}
steps:
- name: Checkout
uses: actions/checkout@v4
fetch-depth: 0
- name: Extract Version
id: version
run: |
VERSION=$(python3 -c "
import sys
sys.path.insert(0, 'scripts/lib')
import extension_utils
info = extension_utils.get_extension_info('src')
print(info.version if info else 'unknown')
")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Detect Platform
id: platform
run: |
python3 scripts/release/detect_platform.py src
- name: Extract Changelog
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANGELOG=$(awk "/^## \[${VERSION}\]/{flag=1;next}/^## \[/{flag=0}flag" CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
test:
name: Run Tests
needs: metadata
if: ${{ !inputs.skip-tests }}
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-test.yml@v1
with:
php-versions: '["8.0", "8.1", "8.2", "8.3"]'
build:
name: Build Release Package
needs: [metadata, test]
if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped')
runs-on: ubuntu-latest
outputs:
package-name: ${{ steps.build.outputs.package-name }}
package-sha256: ${{ steps.checksum.outputs.sha256 }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Package
id: build
run: |
python3 scripts/release/package_extension.py dist
PACKAGE=$(ls dist/*.zip | head -1)
echo "package-name=$(basename $PACKAGE)" >> $GITHUB_OUTPUT
- name: Calculate Checksum
id: checksum
run: |
cd dist
SHA256=$(sha256sum *.zip | awk '{print $1}')
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
- name: Sign Package
if: ${{ secrets.GPG_PRIVATE_KEY != '' }}
run: |
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import
cd dist
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --passphrase-fd 0 \
--armor --detach-sign *.zip
- name: Upload Package
uses: actions/upload-artifact@v4
with:
name: release-package
path: dist/*
retention-days: 90
release:
name: Create GitHub Release
needs: [metadata, build]
runs-on: ubuntu-latest
outputs:
download-url: ${{ steps.create-release.outputs.upload_url }}
steps:
- name: Download Package
uses: actions/download-artifact@v4
with:
name: release-package
path: ./release
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.metadata.outputs.version }}
name: Release ${{ needs.metadata.outputs.version }}
body: ${{ needs.metadata.outputs.changelog }}
draft: false
prerelease: ${{ inputs.release-classification == 'rc' }}
files: ./release/*
- name: Attest Build Provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: ./release/*.zip
Error Handling and Debugging
Debugging Reusable Workflows
Enable Debug Logging:
# Set repository secret
gh secret set ACTIONS_STEP_DEBUG --body "true"
gh secret set ACTIONS_RUNNER_DEBUG --body "true"
Add Debug Steps:
- name: Debug Information
if: ${{ runner.debug == '1' }}
run: |
echo "=== Environment Variables ==="
env | sort
echo "=== GitHub Context ==="
echo '${{ toJson(github) }}'
echo "=== Inputs ==="
echo '${{ toJson(inputs) }}'
echo "=== Secrets (names only) ==="
echo "FTP_SERVER: ${{ secrets.FTP_SERVER != '' && 'SET' || 'NOT SET' }}"
Common Error Patterns and Solutions
Error: "Required input not provided"
Problem:
Error: Required input 'php-versions' was not provided
Solution:
# In reusable workflow, make it optional with default
inputs:
php-versions:
required: false # Changed from true
type: string
default: '["8.0", "8.1", "8.2", "8.3"]'
Error: "Invalid workflow file"
Problem:
Error: .github/workflows/reusable.yml: Invalid workflow file:
Unexpected value 'workflow_call'
Solution:
Ensure workflow file is in .github/workflows/ directory and uses correct syntax:
on:
workflow_call: # Must be at top level under 'on:'
inputs:
...
Error: "Maximum timeout exceeded"
Problem: Workflow runs too long and times out
Solution:
jobs:
long-running-job:
runs-on: ubuntu-latest
timeout-minutes: 120 # Increase from default 360
steps:
...
Performance Monitoring
Add Timing Information:
- name: Start Timer
id: start-time
run: echo "start=$(date +%s)" >> $GITHUB_OUTPUT
- name: Your Task
run: |
# Task logic here
- name: Report Duration
if: always()
run: |
START=${{ steps.start-time.outputs.start }}
END=$(date +%s)
DURATION=$((END - START))
echo "Task completed in ${DURATION} seconds"
# Send to monitoring system
curl -X POST "${{ secrets.METRICS_ENDPOINT }}" \
-d "{\"duration\": $DURATION, \"job\": \"${{ github.job }}\"}"
Testing Reusable Workflows
Local Testing with act
# Install act
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Test workflow locally
act workflow_call \
-s GITHUB_TOKEN="$(gh auth token)" \
-W .github/workflows/reusable-php-quality.yml \
--input php-versions='["8.1"]'
Integration Testing Strategy
-
Create test repository:
gh repo create test-reusable-workflows --private -
Add caller workflow:
name: Test Reusable Workflow on: push: branches: [test/**] jobs: test: uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-php-quality.yml@test-branch with: php-versions: '["8.1"]' secrets: inherit -
Run test:
git checkout -b test/workflow-test git push origin test/workflow-test -
Monitor results:
gh run watch
Migration from Inline to Reusable
Step-by-Step Conversion Guide
- Identify common workflow patterns across repositories
- Extract to reusable workflow in .github-private
- Add input parameters for customization
- Test in isolation with various input combinations
- Create caller workflow in one repository
- Test integration thoroughly
- Roll out gradually to other repositories
- Monitor and iterate based on feedback
Conversion Checklist
- Extract workflow to .github-private
- Convert triggers to
workflow_call - Identify parameters (make inputs)
- Identify secrets (add to secrets section)
- Add outputs if needed
- Test with different input combinations
- Document usage in README
- Create caller workflow
- Test end-to-end
- Deploy to production
Best Practices
1. Naming Conventions
- Prefix reusable workflows with
reusable- - Use descriptive names:
reusable-php-quality.yml, notquality.yml - Use kebab-case for file names
- Use snake_case for inputs/outputs
2. Input Validation
- name: Validate Inputs
run: |
if [ -z "${{ inputs.required-field }}" ]; then
echo "Error: required-field is empty"
exit 1
fi
if [[ ! "${{ inputs.php-version }}" =~ ^[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid PHP version format"
exit 1
fi
3. Comprehensive Outputs
Always provide useful outputs:
outputs:
status:
value: ${{ jobs.main.outputs.status }}
artifacts:
value: ${{ jobs.main.outputs.artifacts }}
duration:
value: ${{ jobs.main.outputs.duration }}
error-message:
value: ${{ jobs.main.outputs.error-message }}
4. Documentation
Document every reusable workflow:
# At the top of the file
# Reusable PHP Quality Workflow
#
# This workflow performs PHP code quality checks including:
# - PHP_CodeSniffer (PHPCS)
# - PHPStan static analysis
# - PHP Compatibility checks
#
# Usage:
# jobs:
# quality:
# uses: org/.github-private/.github/workflows/reusable-php-quality.yml@v1
# with:
# php-versions: '["8.0", "8.1"]'
#
# Inputs:
# php-versions: JSON array of PHP versions to test
# source-directory: Directory to analyze (default: src)
#
# Outputs:
# all-passed: Boolean indicating if all checks passed
#
# Secrets Required:
# None for basic functionality
5. Version Management
Use semantic versioning:
v1- Major version (may include breaking changes)v1.2- Minor version (backward compatible features)v1.2.3- Patch version (bug fixes only)
Tag releases properly:
git tag -a v1.2.3 -m "Release v1.2.3: Fix PHPCS caching"
git push origin v1.2.3
# Update major/minor tags
git tag -fa v1 -m "Update v1 to v1.2.3"
git push origin v1 --force
Troubleshooting
Workflow Not Found
- Ensure .github-private repository has correct permissions
- Verify workflow path is correct
- Check that target branch/tag exists
Secrets Not Available
- Use
secrets: inheritin caller workflow - Configure secrets at organization or repository level
- Verify secret names match between caller and reusable workflow
Inputs Not Passed Correctly
- Check input types (string, boolean, number)
- Verify required vs optional inputs
- Use correct JSON format for arrays
References
- Reusing Workflows
- workflow_call Event
- Calling Reusable Workflows
- GitHub Actions Best Practices
- Workflow Syntax
Last Updated: 2026-01-05
Version: 2.0
Maintainer: DevOps Team