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
29 changes: 27 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ branch-name ──PR──► main ──auto-deploy──► Vercel production
| `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.
Expand Down Expand Up @@ -93,8 +91,35 @@ Also in `Settings → General → Pull Requests`:
- **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.
- `CRON_SECRET` is required (see [Release data freshness](#release-data-freshness)). Vercel sends it as `Authorization: Bearer $CRON_SECRET` on cron invocations. Generate a random value; never commit it.
- **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).

### Release data freshness

`/download` is **prerendered**, not fetched in the browser. `components/DownloadButton.tsx` is a server component that resolves GitHub releases at render time via `lib/releases.ts`, then hands the data to `DownloadButtonClient.tsx` for the interactive bits. Visitors never call GitHub, so the page costs one upstream request per day instead of one per visitor — comfortably under GitHub's 60/hour unauthenticated limit.

Refresh is driven by the cron in `vercel.json`:

```
0 0 * * * → /api/revalidate-releases
```

At midnight UTC it purges the `releases` cache tag and the prerendered `/download` page; the next visitor triggers one fresh fetch. **A new release therefore takes up to a day to appear on the site.** That delay is intentional — a grace period to pull a release that turns out to be problematic before the website advertises it.

Two safety nets:

- `export const revalidate` on `app/download/page.tsx` (24h) refreshes the page even if the cron stops firing. It duplicates `RELEASES_REVALIDATE_SECONDS` in `lib/releases.ts` because Next requires a literal there — change both together.
- If GitHub is down when a refresh runs, the last good render keeps being served; visitors see nothing wrong.

To publish a release immediately, either redeploy or call the endpoint by hand:

```bash
curl -H "Authorization: Bearer $CRON_SECRET" https://<domain>/api/revalidate-releases
```

> [!NOTE]
> Vercel cron schedules are UTC, and on Hobby plans they run at most once a day and fire within the hour of the scheduled time. Both are fine for a daily grace period.

### Rollback

Production is `main`. Two paths:
Expand Down
130 changes: 0 additions & 130 deletions app/api/releases/route.ts

This file was deleted.

37 changes: 37 additions & 0 deletions app/api/revalidate-releases/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { revalidatePath, revalidateTag } from "next/cache";
import { RELEASES_CACHE_TAG } from "../../../lib/releases";

/**
* Cron target, wired to midnight UTC daily in `vercel.json`.
*
* Drops both the cached GitHub response and the prerendered download page, so
* the first visitor after midnight triggers one fresh fetch. New releases are
* therefore picked up with up to a day's delay — a deliberate grace period in
* case a release turns out to be problematic.
*
* Vercel signs cron requests with `Authorization: Bearer $CRON_SECRET`.
*/
export async function GET(request: Request) {
const secret = process.env.CRON_SECRET;

if (!secret) {
console.error("CRON_SECRET is not configured; refusing to revalidate.");
return NextResponse.json(
{ error: "CRON_SECRET is not configured" },
{ status: 500 },
);
}

if (request.headers.get("authorization") !== `Bearer ${secret}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

revalidateTag(RELEASES_CACHE_TAG, "max");
revalidatePath("/download");

return NextResponse.json({
revalidated: true,
at: new Date().toISOString(),
});
}
13 changes: 12 additions & 1 deletion app/download/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
"use client";
import type { Metadata } from "next";
import { Footer, Navbar } from "../../layout";
import DownloadButton from "../../components/DownloadButton";

export const metadata: Metadata = {
title: "Download VoxKit",
description:
"Download the latest VoxKit release for macOS, Windows, or Linux. Built for speech pathology researchers, no command line required.",
};

// Fallback only: the nightly cron at /api/revalidate-releases is what normally
// refreshes this page. Keep in sync with RELEASES_REVALIDATE_SECONDS (24h) --
// Next requires a literal here, so it cannot be imported.
export const revalidate = 86400;

export default function DownloadPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
Expand Down
Loading
Loading