diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bb1b27f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,174 @@ +# Raintree Technology — reusable CI +# +# Called by every repo in the org: +# +# jobs: +# ci: +# uses: raintree-technology/.github/.github/workflows/ci.yml@ +# with: +# package-manager: bun # bun | pnpm | npm +# standard-ref: # same SHA as the reusable workflow ref +# secrets: inherit # private repos; public repos may omit when no optional org secrets are needed +# +# Pipeline: frozen install → pin check → biome → typecheck → test → build +# → gitleaks (full history) → Socket (only when an API key secret exists). +# Steps that depend on a script (`typecheck`, `test`, `build`) skip cleanly when +# the script is absent, so libraries and workers can call the same workflow. +name: Raintree CI + +on: + workflow_call: + inputs: + package-manager: + description: "bun | pnpm | npm" + type: string + default: bun + node-version: + description: "Node version for pnpm/npm repos (and tooling)" + type: string + default: "22" + pnpm-version: + description: "pnpm version activated through Corepack for pnpm repos" + type: string + default: "10.25.0" + bun-version: + description: "Bun version for bun repos" + type: string + default: "1.3.11" + working-directory: + description: "Root of the package being checked" + type: string + default: "." + standard-ref: + description: "Ref in raintree-technology/.github to fetch standard scripts from; pin to the same SHA as this reusable workflow." + type: string + default: main + run-build: + description: "Run the build script (disable for repos whose build needs deploy credentials)" + type: boolean + default: true + secrets: + SOCKET_SECURITY_API_KEY: + required: false + +permissions: + contents: read + +env: + GITLEAKS_VERSION: "8.30.0" + +jobs: + ci: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout (full history for secret scan) + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Bun + if: inputs.package-manager == 'bun' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: ${{ inputs.bun-version }} + + - name: Setup Node + if: inputs.package-manager != 'bun' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ inputs.node-version }} + + - name: Setup pnpm through Corepack + if: inputs.package-manager == 'pnpm' + run: | + corepack enable + corepack prepare "pnpm@${{ inputs.pnpm-version }}" --activate + pnpm --version + + - name: Install (frozen lockfile) + run: | + case "${{ inputs.package-manager }}" in + bun) bun install --frozen-lockfile ;; + pnpm) pnpm install --frozen-lockfile ;; + npm) npm ci ;; + *) echo "unknown package-manager: ${{ inputs.package-manager }}" >&2; exit 1 ;; + esac + + - name: Verify exact-pinned dependencies + env: + STANDARD_REF: ${{ inputs.standard-ref }} + run: | + curl -fsSL "https://raw.githubusercontent.com/raintree-technology/.github/${STANDARD_REF}/scripts/check-pinned-deps.mjs" -o /tmp/check-pinned-deps.mjs + node /tmp/check-pinned-deps.mjs + + - name: Biome check + run: | + if [ -f biome.json ] || [ -f biome.jsonc ]; then + if [ -x node_modules/.bin/biome ]; then + node_modules/.bin/biome ci . + else + echo "::warning::biome config present but @biomejs/biome is not a devDependency; skipping" + fi + else + echo "no biome config; skipping" + fi + + - name: Typecheck + run: | + if node -e "process.exit(require('./package.json').scripts?.typecheck ? 0 : 1)"; then + case "${{ inputs.package-manager }}" in + bun) bun run typecheck ;; + *) npm run typecheck ;; + esac + else + echo "no typecheck script; skipping" + fi + + - name: Test + env: + CI: "true" + run: | + if node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)"; then + case "${{ inputs.package-manager }}" in + bun) bun run test ;; + *) npm run test ;; + esac + else + echo "no test script; skipping" + fi + + - name: Build + if: inputs.run-build + run: | + if node -e "process.exit(require('./package.json').scripts?.build ? 0 : 1)"; then + case "${{ inputs.package-manager }}" in + bun) bun run build ;; + *) npm run build ;; + esac + else + echo "no build script; skipping" + fi + + - name: Secret scan (gitleaks, full history) + working-directory: ${{ github.workspace }} + run: | + curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tar.gz + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + /tmp/gitleaks detect --source . --redact --exit-code 1 + + - name: Socket supply-chain scan + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }} + run: | + if [ -z "$SOCKET_SECURITY_API_KEY" ]; then + echo "::notice::SOCKET_SECURITY_API_KEY not configured; Socket scan skipped (PR gating via the Socket GitHub App still applies once installed)" + exit 0 + fi + npx -y socket@latest scan create --no-interactive . || { + echo "::error::Socket scan failed" + exit 1 + } diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml new file mode 100644 index 0000000..6220374 --- /dev/null +++ b/.github/workflows/drift-check.yml @@ -0,0 +1,52 @@ +# Raintree Technology — reusable standard-drift check +# +# Called by every repo on a schedule (and on PRs touching config): +# +# on: +# schedule: [{cron: "17 6 * * 1"}] # weekly +# workflow_dispatch: +# jobs: +# drift: +# uses: raintree-technology/.github/.github/workflows/drift-check.yml@ +# with: +# standard-ref: # same SHA as the reusable workflow ref +# +# Fails when the repo stops meeting the Raintree standard: missing files, +# unpinned deps, unpinned action refs, stale/absent central-CI wiring. +name: Raintree drift check + +on: + workflow_call: + inputs: + standard-ref: + description: "Ref in raintree-technology/.github to check out standard scripts from; pin to the same SHA as this reusable workflow." + type: string + default: main + +permissions: + contents: read + +jobs: + drift: + runs-on: ubuntu-latest + steps: + - name: Checkout repo under test + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Checkout org standard + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: raintree-technology/.github + ref: ${{ inputs.standard-ref }} + path: .raintree-standard + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + + - name: Run drift check + run: bash .raintree-standard/scripts/drift-check.sh diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..dd4b5b9 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,4 @@ +# Default owner for the org-standard repo. CODEOWNERS is not inherited from +# the .github repo — each repo carries its own copy (vendored by the +# standardization pass). +* @admin-raintree diff --git a/README.md b/README.md index 9e4b60a..68596ba 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,59 @@ Organization-wide GitHub configuration for [Raintree Technology](https://raintre | `SECURITY.md` | Default security policy and vulnerability reporting process | | `PULL_REQUEST_TEMPLATE.md` | Default PR template with checklist | | `ISSUE_TEMPLATE/` | Bug report and feature request templates | +| `CODEOWNERS` | Owner of this repo (CODEOWNERS is **not** inherited — each repo vendors its own) | +| `.github/workflows/ci.yml` | **Reusable CI** every repo calls: frozen install → pin check → biome → typecheck → test → build → gitleaks → Socket | +| `.github/workflows/drift-check.yml` | **Reusable drift check** run on a schedule per repo; fails when a repo stops meeting the standard | +| `scripts/check-pinned-deps.mjs` | Fails on any `^`/`~` range in package.json (root + workspaces) | +| `scripts/drift-check.sh` | The drift-check engine (files, pinning, SHA-pinned actions, CI wiring) | +| `configs/biome.base.jsonc` | Canonical Biome base — vendored per repo, extended by the repo's `biome.json` | +| `configs/tsconfig.base.json` | Canonical strict TypeScript base — vendored per repo | +| `configs/renovate-base.json` | Shared Renovate preset: pin everything, 7-day `minimumReleaseAge`, weekly grouped PRs | +| `templates/README.template.md` | README template (STATUS badge, stack, setup, env vars, scripts, deploy, license) | + +## The Raintree standard (per repo) + +- Exact-pinned dependencies, one lockfile, frozen installs in CI (`save-exact=true`) +- Biome lint+format extending the vendored canonical base +- Strict TypeScript extending the vendored canonical base +- CI calls the reusable workflow here, pinned to a commit SHA +- All GitHub Actions pinned to full commit SHAs — never floating tags +- Renovate/Dependabot with a 7-day cooldown so freshly published (possibly malicious) versions never land same-day +- Zod-validated env module + committed `.env.example`; secrets only in Vercel env / a secret manager +- README from the template with a STATUS badge (live / WIP / archived) +- Branch protection on `main`: PR required, checks required, no force-push, linear history + +### Calling the reusable CI + +```yaml +# .github/workflows/ci.yml in any repo +name: CI +on: + push: {branches: [main]} + pull_request: +jobs: + ci: + uses: raintree-technology/.github/.github/workflows/ci.yml@ + with: + package-manager: bun # bun | pnpm | npm + standard-ref: + secrets: inherit # private repos; public repos may omit this when no optional org secrets are needed +``` + +### Calling the drift check + +```yaml +# .github/workflows/drift-check.yml in any repo +name: Drift check +on: + schedule: [{cron: "17 6 * * 1"}] + workflow_dispatch: +jobs: + drift: + uses: raintree-technology/.github/.github/workflows/drift-check.yml@ + with: + standard-ref: +``` ## How it works diff --git a/configs/biome.base.jsonc b/configs/biome.base.jsonc new file mode 100644 index 0000000..313fad1 --- /dev/null +++ b/configs/biome.base.jsonc @@ -0,0 +1,32 @@ +// Raintree Technology — canonical Biome base. +// Vendored into each repo as biome.base.jsonc; the repo's biome.json extends it: +// { "extends": ["./biome.base.jsonc"], ... } +// This base enforces org invariants (VCS integration, recommended lint rules, +// organized imports). Formatter style (tabs/spaces, quotes, line width) is a +// per-repo choice and intentionally not set here. +{ + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/configs/renovate-base.json b/configs/renovate-base.json new file mode 100644 index 0000000..561070e --- /dev/null +++ b/configs/renovate-base.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": "Raintree Technology shared Renovate preset. Per-repo renovate.json: { \"extends\": [\"github>raintree-technology/.github//configs/renovate-base.json\"] }", + "extends": ["config:recommended", ":pinAllExceptPeerDependencies"], + "minimumReleaseAge": "7 days", + "rangeStrategy": "pin", + "schedule": ["before 9am on monday"], + "prHourlyLimit": 4, + "packageRules": [ + { + "description": "Group non-major updates into one weekly PR", + "matchUpdateTypes": ["minor", "patch"], + "groupName": "non-major dependencies" + }, + { + "description": "Pin GitHub Action digests", + "matchManagers": ["github-actions"], + "pinDigests": true + } + ], + "vulnerabilityAlerts": { + "enabled": true, + "minimumReleaseAge": null + } +} diff --git a/configs/tsconfig.base.json b/configs/tsconfig.base.json new file mode 100644 index 0000000..45323a1 --- /dev/null +++ b/configs/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "_comment": "Raintree Technology canonical strict TypeScript base. Vendored into each repo as tsconfig.base.json and extended by the repo tsconfig: { \"extends\": \"./tsconfig.base.json\" }. Module/target/jsx/paths stay per-repo.", + "compilerOptions": { + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/scripts/check-pinned-deps.mjs b/scripts/check-pinned-deps.mjs new file mode 100644 index 0000000..3cea9ed --- /dev/null +++ b/scripts/check-pinned-deps.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +// Fails if any dependency in package.json (root + workspace packages) uses a +// range specifier. Exact versions only — `workspace:`, `catalog:`, `npm:` with +// an exact version, file/link/git pins are allowed. +import { readFileSync, existsSync, readdirSync, statSync } from "node:fs"; +import { join, dirname } from "node:path"; + +const SECTIONS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]; +const EXACT = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/; + +function isAllowed(spec) { + if (typeof spec !== "string") return false; + if (EXACT.test(spec)) return true; + if (spec.startsWith("workspace:") || spec.startsWith("catalog:")) return true; + if (spec.startsWith("file:") || spec.startsWith("link:") || spec.startsWith("portal:")) return true; + if (spec.startsWith("git+") || spec.startsWith("github:")) return spec.includes("#"); + if (spec.startsWith("npm:")) { + const at = spec.lastIndexOf("@"); + return at > 4 && EXACT.test(spec.slice(at + 1)); + } + return false; +} + +function* packageJsonFiles(root) { + const rootPkg = join(root, "package.json"); + if (existsSync(rootPkg)) yield rootPkg; + // workspace globs: check conventional dirs (apps/*, packages/*) plus declared workspaces + const declared = (() => { + try { + const p = JSON.parse(readFileSync(rootPkg, "utf8")); + const w = Array.isArray(p.workspaces) ? p.workspaces : p.workspaces?.packages; + return Array.isArray(w) ? w : []; + } catch { + return []; + } + })(); + const dirs = new Set( + ["apps", "packages", ...declared.map((g) => g.split("/")[0])].filter( + (d) => d && !d.includes("*") && existsSync(join(root, d)), + ), + ); + for (const d of dirs) { + for (const sub of readdirSync(join(root, d))) { + const pkg = join(root, d, sub, "package.json"); + if (existsSync(pkg) && statSync(pkg).isFile()) yield pkg; + } + } +} + +let bad = 0; +for (const file of packageJsonFiles(process.cwd())) { + const pkg = JSON.parse(readFileSync(file, "utf8")); + for (const section of SECTIONS) { + for (const [name, spec] of Object.entries(pkg[section] ?? {})) { + // peerDependencies legitimately use ranges for libraries + if (section === "peerDependencies") continue; + if (!isAllowed(spec)) { + console.error(`UNPINNED ${file} ${section}.${name} = "${spec}"`); + bad++; + } + } + } +} + +if (bad > 0) { + console.error(`\n${bad} unpinned dependency specifier(s). Use exact versions (save-exact).`); + process.exit(1); +} +console.log("All dependency specifiers exact-pinned."); diff --git a/scripts/drift-check.sh b/scripts/drift-check.sh new file mode 100644 index 0000000..2e3bcd0 --- /dev/null +++ b/scripts/drift-check.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Raintree standard drift check. Run from the root of the repo under test with +# the org .github repo checked out at .raintree-standard/ (or set STANDARD_DIR). +# Checks apply conditionally by repo type; each violation is one FAIL line. +set -u +STANDARD_DIR="${STANDARD_DIR:-.raintree-standard}" +FAILURES=0 +fail() { echo "FAIL: $1"; FAILURES=$((FAILURES + 1)); } +warn() { echo "WARN: $1"; } +ok() { echo "ok: $1"; } + +# ---- universal checks ------------------------------------------------------- +[ -f README.md ] || fail "README.md missing" +if [ -f README.md ]; then + grep -qE 'img\.shields\.io/badge/status-' README.md \ + && ok "README has STATUS badge" \ + || fail "README missing STATUS badge (live/WIP/archived)" +fi + +if git ls-files --error-unmatch .env >/dev/null 2>&1; then + fail ".env is committed to git" +else + ok "no committed .env" +fi + +# every workflow `uses:` must be pinned to a 40-char SHA (local ./ refs and +# docker:// refs excluded) +if [ -d .github/workflows ]; then + UNPINNED=$(grep -rhoE 'uses:\s*[^ ]+@[^ #]+' .github/workflows/ 2>/dev/null \ + | grep -vE '@[0-9a-f]{40}$' \ + | grep -vE 'uses:\s*(\./|docker://)' || true) + if [ -n "$UNPINNED" ]; then + fail "workflow actions not pinned to commit SHAs:"$'\n'"$UNPINNED" + else + ok "all workflow actions SHA-pinned" + fi + grep -rq 'raintree-technology/.github/.github/workflows/ci.yml@' .github/workflows/ 2>/dev/null \ + && ok "central reusable CI wired" \ + || warn "repo does not call the central reusable CI (fine for Python/Swift/shell repos with bespoke CI)" +else + fail "no .github/workflows directory" +fi + +# ---- JS/TS repos ------------------------------------------------------------ +if [ -f package.json ]; then + node "$STANDARD_DIR/scripts/check-pinned-deps.mjs" || fail "unpinned dependencies (see above)" + + LOCKS=0 + for f in bun.lock bun.lockb pnpm-lock.yaml package-lock.json; do + [ -f "$f" ] && LOCKS=$((LOCKS + 1)) + done + [ "$LOCKS" -eq 1 ] && ok "exactly one lockfile" || fail "expected exactly 1 lockfile, found $LOCKS" + + if [ -f biome.json ] || [ -f biome.jsonc ]; then + ok "biome config present" + else + fail "no biome.json/biome.jsonc" + fi + + node -e "process.exit(require('./package.json').engines?.node ? 0 : 1)" 2>/dev/null \ + && ok "engines.node declared" \ + || fail "package.json missing engines.node" +fi + +# ---- Python repos ----------------------------------------------------------- +if [ -f pyproject.toml ]; then + [ -f uv.lock ] || [ -f poetry.lock ] || fail "pyproject.toml without lockfile (uv.lock/poetry.lock)" +fi + +# ---- summary ---------------------------------------------------------------- +echo "" +if [ "$FAILURES" -gt 0 ]; then + echo "DRIFT: $FAILURES violation(s) of the Raintree standard." + exit 1 +fi +echo "No drift detected." diff --git a/templates/README.template.md b/templates/README.template.md new file mode 100644 index 0000000..dcec71d --- /dev/null +++ b/templates/README.template.md @@ -0,0 +1,56 @@ + + +# {{REPO_NAME}} + +![status](https://img.shields.io/badge/status-{{live|WIP|archived}}-{{brightgreen|yellow|lightgrey}}) +![Raintree Technology](https://img.shields.io/badge/Raintree-Technology-1a7f37) + +{{One-line description: what this is and who it's for.}} + +## Stack + +{{e.g. Next.js 16 (App Router) · TypeScript (strict) · Bun · Neon (Postgres) · Drizzle · Better Auth · Vercel · Biome}} + +## Setup + +```bash +{{bun install}} +cp .env.example .env.local # then fill in values +{{bun dev}} +``` + +## Environment variables + +Validated at boot by {{`lib/env.ts`}} (Zod). Real values live in Vercel env / a +secret manager — never in the repo. + +| Variable | Purpose | +| --- | --- | +| `{{DATABASE_URL}}` | {{Neon Postgres connection string}} | + +## Scripts + +| Script | What it does | +| --- | --- | +| `dev` | local dev server | +| `build` | production build | +| `check` | Biome lint + format check | +| `typecheck` | `tsc --noEmit` | +| `test` | test suite | + +## Deploy + +{{Vercel project `{{name}}`; pushes to `main` deploy via PR merge only.}} + +## License + +{{MIT — see [LICENSE](LICENSE) | Proprietary — © FinSync LLC (dba Raintree Technology)}} + +--- + +Built by [Raintree Technology](https://raintree.technology) · [hello@raintree.technology](mailto:hello@raintree.technology)