#!/usr/bin/env python3 """Render .gitea/workflows/pipeline.yml for a peptide-repo repository. Why this exists: Gitea 1.25 cannot run reusable workflows (workflow_call), so the shared CI/deploy logic lives in composite actions under .gitea/actions/ in peptide-e2e, and each repo gets a thin generated job graph that calls them. Edit repos.json or this template, never the generated files. usage: render.py [repos.json] -> prints the workflow to stdout """ import json import sys A = "https://git.protopeer.com/peptide-repo/peptide-e2e/.gitea/actions" DEFAULTS = {"tests": "stubs", "has_js": False, "line_limit_paths": "includes", "enforce_300": True, "phpcs_autofix": True, "audit": True, "php_versions": ["8.1", "8.2", "8.3"], "extras": []} MAIN_PUSH = "github.event_name == 'push' && github.ref == 'refs/heads/main'" EXTRAS = { "check-constants": """ check-constants: name: Check PRAUTOBLOGGER_* constants runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.1' coverage: none # Fails if any PRAUTOBLOGGER_* constant referenced in shipped PHP is neither # defined in prautoblogger.php nor guarded by defined() (v0.19.0 incident class). - run: php scripts/check-constants.php - name: Self-test the scanner run: php scripts/check-constants.php --self-test """, "validate-psa-version": """ validate: name: Validate PSA_VERSION constant runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | if grep -q "define( 'PSA_VERSION'" peptide-search-ai.php; then version=$(grep "define( 'PSA_VERSION'" peptide-search-ai.php | sed "s/.*'\\([^']*\\)'.*/\\1/") echo "PSA_VERSION: $version" else echo "::error::PSA_VERSION not found"; exit 1 fi """, "security-audit": """ security-audit: name: Security Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check for common PHP security issues shell: bash run: | echo ":: Checking for direct SQL queries without prepare..." if grep -r "query\\s*(\\s*['\\"].*\\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null \\ | grep -v "\\$wpdb->prepare" \\ | grep -v "uninstall\\.php" \\ | grep -v "class-peptide-news-activator\\.php" \\ | grep -v "class-peptide-news-logger\\.php" \\ | grep -v "DROP TABLE" \\ | grep -v "TRUNCATE" \\ && [ ${PIPESTATUS[0]} -eq 0 ]; then echo "WARNING: Found potential unescaped SQL queries. Use wpdb->prepare() for all dynamic SQL." exit 1 fi echo ":: Checking for eval() usage..." if grep -r "eval\\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then echo "ERROR: eval() found. This is a critical security risk."; exit 1 fi echo ":: Checking for base64_decode without validation..." if grep -r "base64_decode\\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then echo "WARNING: Found base64_decode(). Ensure the input is properly validated." fi echo ":: Checking for file_get_contents with user input..." if grep -r "file_get_contents\\s*(\\s*\\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then echo "WARNING: Found file_get_contents with variable. Ensure input is validated and restricted." fi echo "All security checks passed." """, } EXTRA_JOB_ID = {"check-constants": "check-constants", "validate-psa-version": "validate", "security-audit": "security-audit"} def render(name, cfg): c = {**DEFAULTS, **cfg} php = json.dumps(c["php_versions"]) out = [f"""# GENERATED by peptide-e2e/tools/gitea-pipeline/render.py from repos.json. # Do not edit by hand: change repos.json or the template and re-render. name: CI / Deploy on: push: branches: [main] pull_request: {{}} workflow_dispatch: {{}} concurrency: group: pipeline-{name}-${{{{ github.ref }}}} cancel-in-progress: false jobs: lint-php: name: PHP Lint (${{{{ matrix.php }}}}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: {php} steps: - uses: actions/checkout@v4 - uses: {A}/ci-lint-php@main with: php: ${{{{ matrix.php }}}} phpcs: name: PHPCS (WordPress standards) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{{{ github.head_ref || github.ref_name }}}} token: ${{{{ secrets.GITHUB_TOKEN }}}} - uses: {A}/ci-phpcs@main with: autofix: '{str(c["phpcs_autofix"]).lower()}' """] ci_jobs = ["lint-php", "phpcs"] if c["enforce_300"]: out.append(f""" file-size: name: 300-line rule ({c["line_limit_paths"]}) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: {A}/ci-file-size@main with: paths: {c["line_limit_paths"]} """) ci_jobs.append("file-size") if c["tests"] != "none": svc = "" if c["tests"] == "wp-framework": svc = """ services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: wp_tests options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 """ out.append(f""" phpunit: name: PHPUnit - {c["tests"]} (PHP ${{{{ matrix.php }}}}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: {php} {svc} steps: - uses: actions/checkout@v4 - uses: {A}/ci-phpunit@main with: php: ${{{{ matrix.php }}}} tests: {c["tests"]} """) ci_jobs.append("phpunit") if c["has_js"]: out.append(f""" lint-js: name: JS Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: {A}/ci-lint-js@main """) ci_jobs.append("lint-js") for x in c["extras"]: out.append(EXTRAS[x]) ci_jobs.append(EXTRA_JOB_ID[x]) if c["target_path"]: needs = json.dumps(ci_jobs) deploy_with = f""" target_path: {c["target_path"]} ssh_private_key: ${{{{ secrets.SSH_PRIVATE_KEY }}}} ssh_host: ${{{{ secrets.SSH_HOST }}}} ssh_port: ${{{{ secrets.SSH_PORT }}}} ssh_username: ${{{{ secrets.SSH_USERNAME }}}}""" out.append(f""" deploy-staging: name: Deploy to staging needs: {needs} if: ${{{{ {MAIN_PUSH} }}}} runs-on: peptide-vps steps: - uses: actions/checkout@v4 - uses: {A}/deploy@main with: stage: staging {deploy_with} smoke: name: Smoke tests (staging) needs: deploy-staging runs-on: peptide-vps timeout-minutes: 15 steps: - uses: {A}/smoke@main with: wp_admin_user: ${{{{ secrets.STAGING_WP_ADMIN_USER }}}} wp_admin_password: ${{{{ secrets.STAGING_WP_ADMIN_PASSWORD }}}} deploy-production: name: Deploy to production # Smoke is a gate: production runs only if staging deploy AND smoke succeeded. # PROD_DEPLOY_ENABLED (org variable) is the cutover switch / kill switch. needs: [deploy-staging, smoke] if: ${{{{ vars.PROD_DEPLOY_ENABLED == 'true' && {MAIN_PUSH} }}}} runs-on: peptide-vps steps: - uses: actions/checkout@v4 - uses: {A}/deploy@main with: stage: production {deploy_with} """) if c["audit"]: out.append(f""" audit-direct-push: name: Audit - commit came from a PR if: ${{{{ {MAIN_PUSH} }}}} runs-on: ubuntu-latest steps: - uses: {A}/audit-direct-push@main with: token: ${{{{ secrets.GITHUB_TOKEN }}}} """) return "".join(out) if __name__ == "__main__": repos = json.load(open(sys.argv[2] if len(sys.argv) > 2 else __file__.rsplit("/", 1)[0] + "/repos.json")) name = sys.argv[1] sys.stdout.write(render(name, repos[name]))