Files
MokoCassiopeia/docs/REUSABLE_WORKFLOWS.md

1453 lines
38 KiB
Markdown

# 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`**
```yaml
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`**
```yaml
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)**
```yaml
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`):
```yaml
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`):
```yaml
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
```yaml
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
```yaml
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_HOST:
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
1. **Single Source of Truth**: Update workflow logic in one place
2. **Version Control**: Pin to specific versions (@v1, @main, @sha)
3. **Testing**: Test changes in .github-private before rolling out
4. **Security**: Keep sensitive logic private
5. **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`):
```yaml
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`):
```yaml
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_HOST:
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_HOST }};
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**:
```yaml
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**:
```yaml
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`):
```yaml
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`):
```yaml
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_HOST:
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):
```yaml
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**:
```yaml
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`
```yaml
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`
```yaml
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_HOST:
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**:
```bash
# Set repository secret
gh secret set ACTIONS_STEP_DEBUG --body "true"
gh secret set ACTIONS_RUNNER_DEBUG --body "true"
```
**Add Debug Steps**:
```yaml
- 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_HOST: ${{ secrets.FTP_HOST != '' && 'SET' || 'NOT SET' }}"
```
### Common Error Patterns and Solutions
#### Error: "Required input not provided"
**Problem**:
```
Error: Required input 'php-versions' was not provided
```
**Solution**:
```yaml
# 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:
```yaml
on:
workflow_call: # Must be at top level under 'on:'
inputs:
...
```
#### Error: "Maximum timeout exceeded"
**Problem**:
Workflow runs too long and times out
**Solution**:
```yaml
jobs:
long-running-job:
runs-on: ubuntu-latest
timeout-minutes: 120 # Increase from default 360
steps:
...
```
### Performance Monitoring
**Add Timing Information**:
```yaml
- 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
```bash
# 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
1. **Create test repository**:
```bash
gh repo create test-reusable-workflows --private
```
2. **Add caller workflow**:
```yaml
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
```
3. **Run test**:
```bash
git checkout -b test/workflow-test
git push origin test/workflow-test
```
4. **Monitor results**:
```bash
gh run watch
```
## Migration from Inline to Reusable
### Step-by-Step Conversion Guide
1. **Identify common workflow patterns** across repositories
2. **Extract to reusable workflow** in .github-private
3. **Add input parameters** for customization
4. **Test in isolation** with various input combinations
5. **Create caller workflow** in one repository
6. **Test integration** thoroughly
7. **Roll out gradually** to other repositories
8. **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`, not `quality.yml`
- Use kebab-case for file names
- Use snake_case for inputs/outputs
### 2. Input Validation
```yaml
- 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:
```yaml
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:
```yaml
# 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:
```bash
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: inherit` in 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](https://docs.github.com/en/actions/using-workflows/reusing-workflows)
- [workflow_call Event](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_call)
- [Calling Reusable Workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow)
- [GitHub Actions Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
---
**Last Updated**: 2026-01-05
**Version**: 2.0
**Maintainer**: DevOps Team