name: repo-pipeline on: workflow_call: inputs: lint_command: description: Command that runs lint checks required: false type: string default: | if which swiftlint > /dev/null; then swiftlint --strict else brew install swiftlint swiftlint --strict fi build_command: description: Command that builds the project required: false type: string default: | xcodebuild build \ -project CamperLogBook.xcodeproj \ -scheme CamperLogBook \ -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ CODE_SIGNING_ALLOWED=NO test_command: description: Command that executes tests required: false type: string default: | xcodebuild test \ -project CamperLogBook.xcodeproj \ -scheme CamperLogBook \ -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \ CODE_SIGNING_ALLOWED=NO jobs: ci: name: ci runs-on: macos-15 steps: - name: Checkout caller repository uses: actions/checkout@v4 - name: Lint shell: bash run: ${{ inputs.lint_command }} - name: Build shell: bash run: ${{ inputs.build_command }} - name: Test shell: bash run: ${{ inputs.test_command }} 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."); }