Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AqpXh3hQMMaC74pTjFq4it Gitea 1.25 cannot run reusable workflows (workflow_call), so the logic of deploy-app.yml, smoke.yml and ci.yml moves into composite actions under .gitea/actions/, and each repo gets a thin pipeline rendered by tools/gitea-pipeline/render.py from repos.json. .github/workflows is left untouched (Gitea ignores it once .gitea/workflows exists).
254 lines
8.1 KiB
Python
254 lines
8.1 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"
|
|
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]))
|