Files
peptide-e2e/tools/gitea-pipeline/render.py
2026-10-24 09:57:31 +00:00

261 lines
8.7 KiB
Python

#!/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 <repo-name> [repos.json] -> prints the workflow to stdout
"""
import json
import sys
A = "https://git.protopeer.com/peptide-repo/peptide-e2e/.gitea/actions"
# CI image with PHP 8.1/8.2/8.3 + Composer preinstalled (peptide-e2e/ci-image).
# Without it setup-php bootstraps a compiler toolchain on every job (5-20 min).
CI_IMAGE = "git.protopeer.com/peptide-repo/ci-php:2026-10"
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'"
# Deploys also run on a manual (workflow_dispatch) run of main, so a failed or
# drifted deploy can be redone without a dummy commit (deploy-health says so).
MAIN_DEPLOY = ("(github.event_name == 'push' || github.event_name == 'workflow_dispatch')"
" && github.ref == 'refs/heads/main'")
EXTRAS = {
"check-constants": """
check-constants:
name: Check PRAUTOBLOGGER_* constants
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use PHP 8.1 (preinstalled in the CI image)
run: update-alternatives --set php /usr/bin/php8.1 && php -v | head -1
# 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_DEPLOY} }}}}
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_DEPLOY} }}}}
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).replace(
" runs-on: ubuntu-latest\n",
f" runs-on: ubuntu-latest\n container: {CI_IMAGE}\n")
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]))