name: repo-pipeline on: workflow_call: inputs: repo_type: description: Repository type (ios, node, python, custom) required: false type: string default: ios lint_command: description: Command that runs lint checks required: false type: string default: "" build_command: description: Command that builds the project required: false type: string default: "" test_command: description: Command that executes tests required: false type: string default: "" jobs: ci-ios: name: ci if: ${{ inputs.repo_type == 'ios' }} runs-on: macos-15 steps: - name: Checkout caller repository uses: actions/checkout@v4 - name: Resolve commands id: resolve shell: bash run: | set -euo pipefail lint="${{ inputs.lint_command }}" build="${{ inputs.build_command }}" test="${{ inputs.test_command }}" if [ -z "$lint" ]; then lint="if which swiftlint > /dev/null; then swiftlint --strict; else brew install swiftlint && swiftlint --strict; fi" fi if [ -z "$build" ]; then build="xcodebuild build -project CamperLogBook.xcodeproj -scheme CamperLogBook -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' CODE_SIGNING_ALLOWED=NO" fi if [ -z "$test" ]; then test="xcodebuild test -project CamperLogBook.xcodeproj -scheme CamperLogBook -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' CODE_SIGNING_ALLOWED=NO" fi { echo "lint<> "$GITHUB_OUTPUT" - name: Lint shell: bash run: ${{ steps.resolve.outputs.lint }} - name: Build shell: bash run: ${{ steps.resolve.outputs.build }} - name: Test shell: bash run: ${{ steps.resolve.outputs.test }} ci-generic: name: ci if: ${{ inputs.repo_type != 'ios' }} 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 }}" if [ -z "$lint" ]; then case "$repo_type" in node) lint="npm run lint --if-present" ;; python) lint="python -m pip install -U pip && pip install ruff && ruff check ." ;; custom) lint="" ;; *) lint="" ;; esac fi if [ -z "$build" ]; then case "$repo_type" in node) build="npm run build --if-present" ;; python) build="" ;; custom) build="" ;; *) build="" ;; esac fi if [ -z "$test" ]; then case "$repo_type" in node) test="npm test --if-present" ;; python) test="pytest -q" ;; custom) test="" ;; *) test="" ;; esac fi { echo "lint<> "$GITHUB_OUTPUT" - name: Lint if: ${{ steps.resolve.outputs.lint != '' }} shell: bash run: ${{ steps.resolve.outputs.lint }} - name: Build if: ${{ steps.resolve.outputs.build != '' }} shell: bash run: ${{ steps.resolve.outputs.build }} - name: Test if: ${{ steps.resolve.outputs.test != '' }} shell: bash run: ${{ steps.resolve.outputs.test }} 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: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Semgrep uses: returntocorp/semgrep-action@v1 with: config: p/default - name: Dependency Review if: github.event_name == 'pull_request' uses: actions/dependency-review-action@v4 ai-review: name: ai-review runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read steps: - 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."); }