feat: add Farming Labs favicon #445
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main, master] | |
| pull_request: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint: | |
| name: Lint, format & typecheck | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm lint | |
| - run: pnpm format:check | |
| - run: pnpm typecheck | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| env: | |
| FARM_ORM_LOCAL_PG_ADMIN_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres | |
| FARM_ORM_LOCAL_MYSQL_ADMIN_URL: mysql://root:root@127.0.0.1:3306 | |
| FARM_ORM_LOCAL_MONGODB_URL: mongodb://127.0.0.1:27017 | |
| FARM_ORM_SKIP_REAL_FIRESTORE_TESTS: "1" | |
| FARM_ORM_DEMO_REQUIRE_LOCAL: "1" | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_DB: postgres | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres -d postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_DATABASE: mysql | |
| MYSQL_ROOT_PASSWORD: root | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 20 | |
| mongodb: | |
| image: mongo:7.0 | |
| ports: | |
| - 27017:27017 | |
| options: >- | |
| --health-cmd "mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm test | |
| - run: pnpm test:local:sql | |
| - run: pnpm test:local:mongodb | |
| - run: pnpm --filter demo test:local | |
| security-audit: | |
| name: Security audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Get pnpm store directory | |
| shell: bash | |
| run: | | |
| echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | |
| - name: Setup pnpm store cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.STORE_PATH }} | |
| key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-store- | |
| - name: Install workspace dependencies | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| pnpm install --frozen-lockfile | |
| - name: Run security audit | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| AUDIT_TREE_FILE="$(mktemp)" | |
| trap 'rm -f "$AUDIT_TREE_FILE"' EXIT | |
| pnpm list -r --json --depth Infinity > "$AUDIT_TREE_FILE" | |
| AUDIT_TREE_FILE="$AUDIT_TREE_FILE" node --input-type=module <<'EOF' | |
| import fs from "node:fs/promises"; | |
| const ADVISORY_URL = | |
| "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk"; | |
| const SEVERITY_ORDER = { | |
| info: 0, | |
| low: 1, | |
| moderate: 2, | |
| high: 3, | |
| critical: 4, | |
| }; | |
| const threshold = SEVERITY_ORDER.high; | |
| const tree = JSON.parse( | |
| await fs.readFile(process.env.AUDIT_TREE_FILE, "utf8"), | |
| ); | |
| const versionsByName = new Map(); | |
| const seen = new Set(); | |
| function add(name, version) { | |
| if (!name || !version) return; | |
| const text = String(version); | |
| if ( | |
| text.startsWith("link:") || | |
| text.startsWith("file:") || | |
| text.startsWith("workspace:") | |
| ) { | |
| return; | |
| } | |
| let versions = versionsByName.get(name); | |
| if (!versions) { | |
| versions = new Set(); | |
| versionsByName.set(name, versions); | |
| } | |
| versions.add(text); | |
| } | |
| function visit(record) { | |
| if (!record || typeof record !== "object") return; | |
| for (const [name, dep] of Object.entries(record)) { | |
| if (!dep || typeof dep !== "object") continue; | |
| add(name, dep.version); | |
| const key = dep.path ?? `${name}@${dep.version}`; | |
| if (seen.has(key)) continue; | |
| seen.add(key); | |
| visit(dep.dependencies); | |
| visit(dep.devDependencies); | |
| visit(dep.optionalDependencies); | |
| visit(dep.peerDependencies); | |
| } | |
| } | |
| for (const workspace of tree) { | |
| add(workspace.name, workspace.version); | |
| visit(workspace.dependencies); | |
| visit(workspace.devDependencies); | |
| visit(workspace.optionalDependencies); | |
| visit(workspace.peerDependencies); | |
| } | |
| const entries = [...versionsByName.entries()].sort(([left], [right]) => | |
| left.localeCompare(right), | |
| ); | |
| const findings = []; | |
| for (let index = 0; index < entries.length; index += 250) { | |
| const payload = Object.fromEntries( | |
| entries | |
| .slice(index, index + 250) | |
| .map(([name, versions]) => [name, [...versions].sort()]), | |
| ); | |
| const response = await fetch(ADVISORY_URL, { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| throw new Error( | |
| `npm bulk advisory request failed with ${response.status}: ${await response.text()}`, | |
| ); | |
| } | |
| const advisoriesByName = await response.json(); | |
| for (const [name, advisories] of Object.entries(advisoriesByName)) { | |
| if (!Array.isArray(advisories)) continue; | |
| for (const advisory of advisories) { | |
| const severity = advisory.severity ?? "info"; | |
| if ((SEVERITY_ORDER[severity] ?? -1) < threshold) continue; | |
| findings.push({ | |
| name, | |
| severity, | |
| title: advisory.title, | |
| vulnerableVersions: advisory.vulnerable_versions, | |
| url: advisory.url, | |
| }); | |
| } | |
| } | |
| } | |
| console.log( | |
| `Scanned ${entries.length} package names using npm bulk advisories.`, | |
| ); | |
| if (findings.length === 0) { | |
| console.log("No high or critical vulnerabilities found"); | |
| process.exit(0); | |
| } | |
| findings.sort((left, right) => { | |
| const severityDelta = | |
| (SEVERITY_ORDER[right.severity] ?? -1) - | |
| (SEVERITY_ORDER[left.severity] ?? -1); | |
| if (severityDelta !== 0) return severityDelta; | |
| return left.name.localeCompare(right.name); | |
| }); | |
| console.error( | |
| `Found ${findings.length} high/critical vulnerabilit${findings.length === 1 ? "y" : "ies"}`, | |
| ); | |
| for (const finding of findings) { | |
| console.error( | |
| `- [${finding.severity}] ${finding.name}: ${finding.title} (${finding.vulnerableVersions})`, | |
| ); | |
| console.error(` ${finding.url}`); | |
| } | |
| process.exit(1); | |
| EOF |