Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.github/workflows/*.lock.yml linguist-generated=true merge=ours
114 changes: 114 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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 <short-descriptive-name>`
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 <sha>` → 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.
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
npx lint-staged
npm run format:check
npm run typecheck
4 changes: 4 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
npm run lint
npm run format:check
npm run typecheck
npm run build
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.next/
out/
build/
node_modules/
next-env.d.ts
public/
tsconfig.tsbuildinfo
package-lock.json
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 15 additions & 15 deletions app/api/releases/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextResponse } from 'next/server';
import { NextResponse } from "next/server";
import type {
GitHubRelease,
GitHubAsset,
Expand All @@ -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<OperatingSystem, RegExp> = {
macos: /\.(dmg|pkg)$/i,
Expand All @@ -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;
Expand All @@ -50,33 +50,33 @@ 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;
const repo = process.env.GITHUB_REPO;

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 },
);
}

Expand Down Expand Up @@ -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 },
);
}
}
31 changes: 14 additions & 17 deletions app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,24 @@ import { Footer, Navbar } from "../../layout";
export default function DocsPage() {
return (
<>
<div className="h-screen flex flex-col bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white overflow-hidden">
<Navbar view="Docs" />
<div className="h-screen flex flex-col bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white overflow-hidden">
<Navbar view="Docs" />

{/* Documentation iframe container */}
<div className="flex-1 px-4 sm:px-6 lg:px-8 pt-24 pb-8 overflow-hidden">
<div className="max-w-7xl w-full h-full mx-auto">

{/* Iframe wrapper with styling */}
<div className="h-full rounded-lg overflow-hidden shadow-2xl border border-slate-700 bg-white">
<iframe
src="/docs/index.html"
className="w-full h-full border-none"
title="VoxKit API Documentation"
/>
{/* Documentation iframe container */}
<div className="flex-1 px-4 sm:px-6 lg:px-8 pt-24 pb-8 overflow-hidden">
<div className="max-w-7xl w-full h-full mx-auto">
{/* Iframe wrapper with styling */}
<div className="h-full rounded-lg overflow-hidden shadow-2xl border border-slate-700 bg-white">
<iframe
src="/docs/index.html"
className="w-full h-full border-none"
title="VoxKit API Documentation"
/>
</div>
</div>
</div>
</div>


</div>
<Footer />
<Footer />
</>
);
}
8 changes: 4 additions & 4 deletions app/download/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"
import { Footer, Navbar } from '../../layout';
import DownloadButton from '../../components/DownloadButton';
"use client";
import { Footer, Navbar } from "../../layout";
import DownloadButton from "../../components/DownloadButton";

export default function DownloadPage() {
return (
Expand All @@ -20,4 +20,4 @@ export default function DownloadPage() {
<Footer />
</div>
);
}
}
Loading
Loading