commit 9adebedf023d024ef28641fcecbaa200408e5c9c Author: Oliver G Date: Sat Mar 7 11:44:59 2026 +0100 Initial reusable pipeline (ci, security-scan, ai-review) diff --git a/.github/workflows/repo-pipeline.yml b/.github/workflows/repo-pipeline.yml new file mode 100644 index 0000000..3728c31 --- /dev/null +++ b/.github/workflows/repo-pipeline.yml @@ -0,0 +1,151 @@ +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."); + } diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe829c4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# vanity-dev-engine + +Shared CI/Security/AI reusable workflows for Vanity ecosystem repositories. + +## Reusable Workflow + +Use from another repository: + +```yaml +jobs: + use-vanity-dev-engine: + uses: OliverGiertz/vanity-dev-engine/.github/workflows/repo-pipeline.yml@v1 + secrets: inherit +``` + +Optional inputs: + +- `lint_command` +- `build_command` +- `test_command` + +Optional control: + +- set repository variable `USE_VANITY_DEV_ENGINE=true` in consumer repos.