vanity-dev-engine/.github/workflows/repo-pipeline.yml

242 lines
7.6 KiB
YAML

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<<EOF"
echo "$lint"
echo "EOF"
echo "build<<EOF"
echo "$build"
echo "EOF"
echo "test<<EOF"
echo "$test"
echo "EOF"
} >> "$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<<EOF"
echo "$lint"
echo "EOF"
echo "build<<EOF"
echo "$build"
echo "EOF"
echo "test<<EOF"
echo "$test"
echo "EOF"
} >> "$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.");
}