diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1965c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..ab8ae1a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing + +> [!NOTE] +> This guide serves to track changes to CI/CD and inform contributors and new maintainers of those patterns so they aren't repeated or causing confusion during development. + +`main` is the code deployed to production, Vercel auto-deploys every commit to it. All human changes reach `main` through a pull request (no direct pushes). The one exception is the sync-docs automation bot, which pushes generated documentation commits (`sync: update documentation from voxkit-desktop`) directly to `main` when new application code is deployed from [voxkit-desktop](https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop). + +This contribution guide is split by role: + +- **[For developers](#for-developers)**: writing and shipping code. +- **[For repo & Vercel owners](#for-repo--vercel-owners)**: setting the guardrails that control inflow. + +--- + +## For developers + +- **Internal members** (collaborators with write access) create a branch in this repo. +- **External contributors** fork the repo, branch in the fork, and open a PR from the fork against `main`. + +### Contribution model (GitHub Flow) + +``` +branch-name ──PR──► main ──auto-deploy──► Vercel production +``` + +1. Branch off `main` (in this repo if internal, in your fork if external): `git checkout -b ` +2. Commit work locally, the pre-commit hook enforces lint-staged (lint + format on staged files) + repo-wide format:check + typecheck on every commit. +3. Push the branch; the pre-push hook enforces full lint + format:check + typecheck + `next build`, so a broken branch doesn't even reach GitHub. +4. Open a PR against `main`. Vercel posts a preview URL on the PR. +5. Merge via Squash and merge once review + checks pass. Keeps `main` history linear; 1 commit = 1 shipped change. +6. Delete the branch after merge. Vercel deploys the new `main` to production automatically. + +### Useful scripts + +| Command | What it does | +| ---------------------- | --------------------------------------------- | +| `npm run dev` | Local dev server (`USE_FAKE_RELEASES=false`). | +| `npm run build` | Production build. | +| `npm run lint` | ESLint over the project. | +| `npm run typecheck` | `tsc --noEmit`. | +| `npm run format` | Prettier-format the whole project. | +| `npm run format:check` | Prettier check (no writes); fails on drift. | + +--- + +## For repo & Vercel owners + +You own the guardrails that make the developer workflow safe. The rules below are authoritative. + +### GitHub branch protection (`main`) + +We use **Rulesets**, not the legacy "Branch protection rules". Rulesets supersede branch protection and are required for app-based bypass (see [Automation bot bypass](#automation-bot-bypass) below). + +`Settings → Rules → Rulesets → New branch ruleset` + +Configure the ruleset: + +- **Name**: `main protection` (or similar). +- **Enforcement status**: `Active`. +- **Target branches**: `Include default branch` (or add `refs/heads/main` explicitly). +- **Bypass list**: add the **Repository admin** role so the someone can land emergency fixes or rollbacks if PR review is unavailable. The sync-docs bot is added in the next section. No other humans should be on this list. + +Branch rules to enable: + +- **Restrict deletions**: prevents `main` from being deleted. +- **Require linear history**: pairs with squash-merge to keep history flat. +- **Require a pull request before merging**: + - Required approvals: **1**. + - **Dismiss stale pull request approvals when new commits are pushed**: on. + - **Require approval of the most recent reviewable push**: on. +- **Require status checks to pass**: + - **Require branches to be up to date before merging**: on. + - Add each required check by name once the CI workflow exists (lint, typecheck, format, Vercel). +- **Block force pushes**: on. + +Anything not in the Bypass list is subject to all of the above. The Repository admin can bypass for genuine emergencies; routine work still goes through PRs. + +### Automation bot bypass + +The docs-sync workflow in [`voxkit-desktop`](https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop) pushes generated docs to `main` here using `secrets.PRIVATE_REPO_TOKEN`. The push is authenticated as the **owner of that PAT**, not as `github-actions[bot]`, so the PAT must be issued by the Repository admin (already in the Bypass list above). No separate bypass entry is needed. + +Also in `Settings → General → Pull Requests`: + +- **Allow squash merging**: on. +- **Allow merge commits**: off. +- **Allow rebase merging**: off. +- **Automatically delete head branches**: on. + +### Vercel project + +- **Production branch**: `main`. Every commit triggers a production deployment. +- **Preview deployments**: enabled for every PR and every non-`main` branch push. Vercel posts the preview URL as a PR check. +- **Environment variables**: managed in the Vercel dashboard. + - Production secrets must be scoped **Production only** so PR previews can't read them. + - Preview-safe variables can be scoped to Preview + Development. +- **GitHub integration**: the Vercel GitHub App must have access to this repo so it can post deployment statuses (these become required checks in branch protection). + +### Rollback + +Production is `main`. Two paths: + +1. **Preferred**: revert the bad commit on `main` via a PR; `git revert ` → PR → squash-merge. Vercel auto-redeploys. History stays linear and the revert is auditable. +2. **Emergency**: in the Vercel dashboard, promote a prior production deployment to current. Do this when a revert PR would be too slow. Immediately follow with a revert PR so `main` and production are back in sync. + +### Enforcement summary + +| Stage | Enforced by | Gate | Authoritative? | +| ----------------- | ------------------------ | --------------------------------------------------- | ------------------ | +| Each commit | Husky `pre-commit` | lint-staged + format:check + typecheck | No (`--no-verify`) | +| Each push | Husky `pre-push` | full lint + format:check + typecheck + `next build` | No (`--no-verify`) | +| Reaching `main` | GitHub branch protection | PR + approval + CI checks | **Yes** | +| Production deploy | Vercel | auto on `main` | **Yes** | + +The hooks make the common case fast and pleasant. Branch protection is what actually keeps `main` clean. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3fa4ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + + typecheck: + name: typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run typecheck + + format: + name: format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run format:check diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..4913520 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +npx lint-staged +npm run format:check +npm run typecheck diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..d434665 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +npm run lint +npm run format:check +npm run typecheck +npm run build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..20ed00c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +.next/ +out/ +build/ +node_modules/ +next-env.d.ts +public/ +tsconfig.tsbuildinfo +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..98349d5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +See [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) for the contribution guide, CI/CD layout, and guardrails. Agents should follow the same rules as human contributors. diff --git a/app/api/releases/route.ts b/app/api/releases/route.ts index 1598453..5ef663c 100644 --- a/app/api/releases/route.ts +++ b/app/api/releases/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import type { GitHubRelease, GitHubAsset, @@ -7,8 +7,8 @@ import type { OperatingSystem, ReleaseAsset, ReleasesAPIResponse, -} from '../../../types/releases'; -import { fakeGitHubReleases } from '../../../lib/fakeReleases'; +} from "../../../types/releases"; +import { fakeGitHubReleases } from "../../../lib/fakeReleases"; const OS_EXTENSIONS: Record = { macos: /\.(dmg|pkg)$/i, @@ -29,8 +29,8 @@ function parseVersion(tag: string): string | null { } function compareVersions(v1: string, v2: string): number { - const parts1 = v1.split('.').map(Number); - const parts2 = v2.split('.').map(Number); + const parts1 = v1.split(".").map(Number); + const parts2 = v2.split(".").map(Number); for (let i = 0; i < 3; i++) { if (parts1[i] > parts2[i]) return 1; if (parts1[i] < parts2[i]) return -1; @@ -50,7 +50,7 @@ export async function GET() { try { let releases: GitHubRelease[]; - if (process.env.USE_FAKE_RELEASES === 'true') { + if (process.env.USE_FAKE_RELEASES === "true") { releases = fakeGitHubReleases; } else { const owner = process.env.GITHUB_OWNER; @@ -58,25 +58,25 @@ export async function GET() { if (!owner || !repo) { return NextResponse.json( - { error: 'GITHUB_OWNER or GITHUB_REPO is not configured' }, - { status: 500 } + { error: "GITHUB_OWNER or GITHUB_REPO is not configured" }, + { status: 500 }, ); } const response = await fetch( `https://api.github.com/repos/${owner}/${repo}/releases`, { - headers: { Accept: 'application/vnd.github+json' }, + headers: { Accept: "application/vnd.github+json" }, next: { revalidate: 5000 }, - } + }, ); if (!response.ok) { const errorText = await response.text(); - console.error('GitHub API error:', response.status, errorText); + console.error("GitHub API error:", response.status, errorText); return NextResponse.json( { error: `Failed to fetch releases from GitHub: ${response.status}` }, - { status: response.status } + { status: response.status }, ); } @@ -121,10 +121,10 @@ export async function GET() { return NextResponse.json(apiResponse); } catch (error) { - console.error('Error fetching releases:', error); + console.error("Error fetching releases:", error); return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } + { error: "Internal server error" }, + { status: 500 }, ); } } diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 4cab87e..f4db611 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -4,27 +4,24 @@ import { Footer, Navbar } from "../../layout"; export default function DocsPage() { return ( <> -
- +
+ - {/* Documentation iframe container */} -
-
- - {/* Iframe wrapper with styling */} -
-