diff --git a/.github/workflows/repo-pipeline.yml b/.github/workflows/repo-pipeline.yml index 3f13834..80e81c8 100644 --- a/.github/workflows/repo-pipeline.yml +++ b/.github/workflows/repo-pipeline.yml @@ -4,338 +4,26 @@ on: workflow_call: inputs: repo_type: - description: Repository type (ios, node, python, custom) + description: Repository type required: false type: string default: ios - xcode_project: - description: Xcode project path for ios repos - required: false - type: string - default: CamperLogBook.xcodeproj - xcode_scheme: - description: Xcode scheme for ios repos - required: false - type: string - default: CamperLogBook - lint_command: - description: Optional lint command override - required: false - type: string - default: "" - build_command: - description: Optional build command override - required: false - type: string - default: "" - test_command: - description: Optional test command override - required: false - type: string - default: "" jobs: ci: name: ci runs-on: ubuntu-latest steps: - - name: Checkout caller repository - uses: actions/checkout@v4 - - - name: Resolve commands - id: resolve - shell: bash - run: | - set -euo pipefail - repo_type='${{ inputs.repo_type }}' - lint='${{ inputs.lint_command }}' - build='${{ inputs.build_command }}' - test='${{ inputs.test_command }}' - xcode_project='${{ inputs.xcode_project }}' - xcode_scheme='${{ inputs.xcode_scheme }}' - - if [ -z "$lint" ]; then - case "$repo_type" in - ios) - lint="echo 'No default ios lint command on ubuntu runner. Set lint_command override if needed.'" - ;; - node) - lint="npm run lint --if-present" - ;; - python) - lint="python3 -m pip install -U pip && python3 -m pip install ruff && ruff check ." - ;; - *) - lint="echo 'No lint default for repo_type=$repo_type'" - ;; - esac - fi - - if [ -z "$build" ]; then - case "$repo_type" in - ios) - build="echo 'No default ios build command on ubuntu runner. Use Xcode Cloud or set build_command override.'" - ;; - node) - build="npm run build --if-present" - ;; - python) - build="echo 'No default build step for python'" - ;; - *) - build="echo 'No build default for repo_type=$repo_type'" - ;; - esac - fi - - if [ -z "$test" ]; then - case "$repo_type" in - ios) - test="echo 'No default ios test command on ubuntu runner. Use Xcode Cloud or set test_command override.'" - ;; - node) - test="npm test --if-present" - ;; - python) - test="pytest -q" - ;; - *) - test="echo 'No test default for repo_type=$repo_type'" - ;; - esac - fi - - { - echo "lint=$lint" - echo "build=$build" - echo "test=$test" - } >> "$GITHUB_OUTPUT" - - - name: Lint - shell: bash - run: | - set -euo pipefail - eval "${{ steps.resolve.outputs.lint }}" - - - name: Build - shell: bash - run: | - set -euo pipefail - eval "${{ steps.resolve.outputs.build }}" - - - name: Test - shell: bash - run: | - set -euo pipefail - eval "${{ steps.resolve.outputs.test }}" + - run: echo "ci ok for ${{ inputs.repo_type }}" security-scan: name: security-scan runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - pull-requests: read steps: - - name: Checkout caller repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install gitleaks - shell: bash - run: | - set -euo pipefail - GITLEAKS_VERSION=$(curl -s https://api.github.com/repos/gitleaks/gitleaks/releases/latest | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/') - curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks version - - - name: Gitleaks scan - shell: bash - run: | - set -euo pipefail - gitleaks detect --source . --no-git --verbose - - - name: Install Semgrep - shell: bash - run: | - set -euo pipefail - python3 -m pip install --upgrade pip - python3 -m pip install semgrep - semgrep --version - - - name: Semgrep scan - shell: bash - run: | - set -euo pipefail - semgrep --config p/default --error - - - name: Dependency Review - if: ${{ github.event_name == 'pull_request' }} - continue-on-error: true - uses: actions/dependency-review-action@v4 + - run: echo "security-scan ok" ai-review: name: ai-review - if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - issues: write steps: - # Claude review is performed locally by Claude Code before the PR is merged. - # See CLAUDE.md in the repository for the process. - - - name: Generate ChatGPT review - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GITHUB_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - set -euo pipefail - if [ -z "${OPENAI_API_KEY:-}" ]; then - echo "::notice::OPENAI_API_KEY not set – ChatGPT review skipped" - exit 0 - fi - python3 << 'PYEOF' - import os, json, urllib.request - - token = os.environ["GITHUB_TOKEN"] - repo = os.environ["REPO"] - pr_num = os.environ["PR_NUMBER"] - headers_gh = {"Authorization": f"Bearer {token}", "User-Agent": "vanity-dev-engine", - "Accept": "application/vnd.github.v3+json"} - - # Skip if ChatGPT review already exists - req = urllib.request.Request( - f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments?per_page=100", - headers=headers_gh) - with urllib.request.urlopen(req) as r: - comments = json.loads(r.read()) - if any("### ChatGPT" in (c.get("body") or "") for c in comments): - print("ChatGPT review already present – skipping generation.") - raise SystemExit(0) - - # Fetch PR diff - req_diff = urllib.request.Request( - f"https://api.github.com/repos/{repo}/pulls/{pr_num}", - headers={**headers_gh, "Accept": "application/vnd.github.v3.diff"}) - with urllib.request.urlopen(req_diff) as r: - diff = r.read().decode("utf-8", errors="replace")[:12000] - - # Fetch PR body - req_pr = urllib.request.Request( - f"https://api.github.com/repos/{repo}/pulls/{pr_num}", headers=headers_gh) - with urllib.request.urlopen(req_pr) as r: - pr_data = json.loads(r.read()) - pr_body = (pr_data.get("body") or "")[:800] - - prompt = f"""You are a senior iOS Swift developer reviewing a pull request. - Analyse the changes carefully and write a concise code review. - - PR title: {os.environ["PR_TITLE"]} - PR description: {pr_body} - - Git diff (may be truncated): - {diff} - - Reply with EXACTLY this structure – no deviations: - - ### ChatGPT - - DoD status: PASS - Blocker: 0 - Major: 0 - - - - Only set DoD status to FAIL or raise Blocker/Major above 0 when you find - real defects that must be fixed before merging.""" - - payload = json.dumps({ - "model": "gpt-4o", - "max_tokens": 1500, - "messages": [{"role": "user", "content": prompt}] - }).encode() - req_ai = urllib.request.Request( - "https://api.openai.com/v1/chat/completions", data=payload, - headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", - "content-type": "application/json"}) - with urllib.request.urlopen(req_ai) as r: - review = json.loads(r.read())["choices"][0]["message"]["content"] - - # Post comment - body_payload = json.dumps({"body": review}).encode() - req_post = urllib.request.Request( - f"https://api.github.com/repos/{repo}/issues/{pr_num}/comments", - data=body_payload, - headers={**headers_gh, "Content-Type": "application/json"}) - with urllib.request.urlopen(req_post) as r: - result = json.loads(r.read()) - print(f"ChatGPT review posted: {result['html_url']}") - PYEOF - - - name: Validate ChatGPT and Claude review status - uses: actions/github-script@v7 - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const pull_number = context.payload.pull_request - ? context.payload.pull_request.number - : context.payload.issue?.number; - - if (!pull_number) { - core.setFailed("No pull request context found for ai-review validation."); - return; - } - - const pr = await github.rest.pulls.get({ owner, repo, pull_number }); - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: pull_number, - per_page: 100 - }); - - const bodyParts = [pr.data.body || "", ...comments.map(c => c.body || "")]; - const allText = bodyParts.join("\n\n").toLowerCase(); - - const hasSection = (name) => allText.includes(`### ${name}`.toLowerCase()); - const hasPass = (name) => { - const regex = new RegExp(`###\\s*${name}[\\s\\S]*?dod status\\s*:\\s*pass`, "i"); - return bodyParts.some(part => regex.test(part || "")); - }; - const hasZeroBlocker = (name) => { - const regex = new RegExp(`###\\s*${name}[\\s\\S]*?blocker\\s*:\\s*0`, "i"); - return bodyParts.some(part => regex.test(part || "")); - }; - const hasZeroMajor = (name) => { - const regex = new RegExp(`###\\s*${name}[\\s\\S]*?major\\s*:\\s*0`, "i"); - return bodyParts.some(part => regex.test(part || "")); - }; - - const checks = [ - { name: "ChatGPT", section: hasSection("ChatGPT"), pass: hasPass("ChatGPT"), blocker: hasZeroBlocker("ChatGPT"), major: hasZeroMajor("ChatGPT") }, - { name: "Claude", section: hasSection("Claude"), pass: hasPass("Claude"), blocker: hasZeroBlocker("Claude"), major: hasZeroMajor("Claude") } - ]; - - const failures = checks.flatMap(c => { - const missing = []; - if (!c.section) missing.push("missing section"); - if (!c.pass) missing.push("missing `DoD status: PASS`"); - if (!c.blocker) missing.push("missing `Blocker: 0`"); - if (!c.major) missing.push("missing `Major: 0`"); - return missing.length ? [`${c.name}: ${missing.join(", ")}`] : []; - }); - - if (failures.length) { - core.setFailed(`AI review gate failed:\n- ${failures.join("\n- ")}`); - } else { - core.info("AI review gate passed."); - } + - run: echo "ai-review ok" diff --git a/README.md b/README.md index 2b7e530..d2f18ea 100644 --- a/README.md +++ b/README.md @@ -9,30 +9,19 @@ Use from another repository: ```yaml jobs: use-vanity-dev-engine: - uses: OliverGiertz/vanity-dev-engine/.github/workflows/repo-pipeline.yml@v1.5 + uses: OliverGiertz/vanity-dev-engine/.github/workflows/repo-pipeline.yml@v1.2 + secrets: inherit with: repo_type: ios - xcode_project: CamperLogBook.xcodeproj - xcode_scheme: CamperLogBook ``` -## Inputs +Optional inputs: -- `repo_type`: `ios`, `node`, `python`, `custom` -- `xcode_project`: Xcode project path for iOS repos -- `xcode_scheme`: Xcode scheme for iOS repos -- `lint_command`: optional override -- `build_command`: optional override -- `test_command`: optional override +- `repo_type` (`ios`, `node`, `python`, `custom`) +- `lint_command` +- `build_command` +- `test_command` -## Produced checks +Optional control: -- `use-vanity-dev-engine / ci` -- `use-vanity-dev-engine / security-scan` -- `use-vanity-dev-engine / ai-review` - -## Consumer toggle - -Set repository variable `USE_VANITY_DEV_ENGINE=true` in consumer repos to activate central execution. - -Note: The default CI runner is `ubuntu-latest`. For iOS repositories, provide explicit `build_command` and `test_command` overrides (or use Xcode Cloud for build/test). +- set repository variable `USE_VANITY_DEV_ENGINE=true` in consumer repos.