Files
MokoCassiopeia/docs/REUSABLE_WORKFLOWS.md
2026-01-05 04:06:17 +00:00

36 KiB

Reusable Workflow Templates for .github-private

This directory contains example templates for reusable workflows that will be moved to the .github-private repository.

Structure

.github-private/
├── .github/
│   └── workflows/
│       ├── reusable-php-quality.yml
│       ├── reusable-release-pipeline.yml
│       ├── reusable-joomla-testing.yml
│       └── reusable-deploy-staging.yml
└── docs/
    └── USAGE.md

Usage in Main Repositories

Basic Pattern

name: Workflow Name

on:
  push:
    branches: [ main ]

jobs:
  job-name:
    uses: mokoconsulting-tech/.github-private/.github/workflows/reusable-workflow-name.yml@main
    with:
      # Input parameters
      parameter-name: value
    secrets: inherit

Example: PHP Quality Check

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_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):

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_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:

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_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):

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_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:

# 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_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:

# 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

  1. Create test repository:

    gh repo create test-reusable-workflows --private
    
  2. 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
    
  3. Run test:

    git checkout -b test/workflow-test
    git push origin test/workflow-test
    
  4. Monitor results:

    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

- 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: 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


Last Updated: 2026-01-05
Version: 2.0
Maintainer: DevOps Team