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
22 changes: 16 additions & 6 deletions docs/operations/github-app-setup.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Configuración de GitHub App (sincronización de repositorios)

HubForge sincroniza issues, pull requests y commits mediante una **GitHub App** (no personal access tokens). El login de auth sigue siendo Supabase GitHub OAuth y es independiente.
HubForge sincroniza issues, pull requests, commits y check runs mediante una **GitHub App** (no personal access tokens).

Eso es independiente del **login de usuarios**: el inicio de sesión usa una OAuth App de GitHub configurada en Supabase Authentication. Credenciales, callbacks y eventos no se comparten entre ambos sistemas.

Rutas de sincronización:

1. **Webhooks** — actualizaciones incrementales cuando GitHub dispara eventos
2. **API backfill** — tras vincular (o mediante **Sync now**), HubForge usa el JWT de la App + installation token para importar issues recientes (~50), PRs (~40) y commits (~40)
2. **API backfill** — tras vincular (o mediante **Sync now**), HubForge usa el JWT de la App + installation token para importar issues recientes (~50), PRs (~40), commits (~40) y check runs (best-effort)

## 1. Crear la GitHub App

Expand All @@ -19,9 +21,12 @@ Rutas de sincronización:
- Metadata: Read-only
- Pull requests: Read-only
- Contents: Read-only (commit metadata)
7. Subscribe to events: `Issues`, `Pull request`, `Push`, `Installation`, `Installation repositories`
- Checks: Read-only (check runs / check suites)
7. Subscribe to events: `Issues`, `Pull request`, `Push`, `Installation`, `Installation repositories`, `Check run`, `Check suite`
8. Crear la app y anotar App ID, Client ID, Client secret; generar una private key

El estado live de eventos y permisos de la App **no está verificado** en esta auditoría. No la modifiques todavía.

## 2. Entorno

```bash
Expand All @@ -38,9 +43,13 @@ SUPABASE_SERVICE_ROLE_KEY=

## 3. Base de datos

Aplica `supabase/migrations/20260728240000_github_app_sync.sql` y
`supabase/migrations/20260803200000_operations_history_deps_github_activity.sql`
después de las migraciones anteriores de HubForge.
Aplica, en orden y solo cuando se autorice, las migraciones de GitHub App:

1. `supabase/migrations/20260728240000_github_app_sync.sql`
2. `supabase/migrations/20260803200000_operations_history_deps_github_activity.sql`
3. `supabase/migrations/20260804120000_github_synced_check_runs.sql`

La migración `20260804120000_github_synced_check_runs.sql` está en el repositorio. **No está confirmada en el proyecto remoto** hasta listarla con la CLI de Supabase autenticada. No la apliques en esta fase de auditoría.

## 4. Vincular un repositorio

Expand All @@ -52,5 +61,6 @@ después de las migraciones anteriores de HubForge.
6. Abre/cierra un issue en GitHub; HubForge hace upsert en `github_synced_issues` y refleja una tarea de HubForge
7. Abre o actualiza un pull request; HubForge hace upsert en `github_synced_pull_requests`
8. Haz push de commits; HubForge hace upsert de filas en `github_synced_commits`
9. Tras aplicar la migración de check runs y suscribir `Check run` / `Check suite`, HubForge hace upsert en `github_synced_check_runs` (best-effort; un fallo no tumba el resto de la sync)

Sin credenciales de App o un installation id, aún puedes vincular un repositorio para mostrarlo, pero el API backfill y **Sync now** permanecen deshabilitados hasta que ambos estén configurados.
78 changes: 50 additions & 28 deletions docs/operations/production-checklist.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,69 @@
# Checklist de producción (MVP usable con env)

Dominio de producción objetivo: `https://hubforge-six.vercel.app`
Proyecto Supabase: `pnpkgfhpwvdkhbncfwqz` (eu-west-1) — esquema/migraciones ya aplicadas.
Proyecto Supabase: `pnpkgfhpwvdkhbncfwqz` (eu-west-1)

## Estado actual (instantánea local)
Auth de usuario (GitHub OAuth vía Supabase) y la GitHub App (sincronización de repositorios) son sistemas independientes. Configurar uno no habilita el otro.

| Pieza | Estado |
| ------------------------------------------------------ | ------------------------------ |
| Supabase Postgres + tablas RLS | Hecho |
| `NEXT_PUBLIC_SUPABASE_*` + `NEXT_PUBLIC_APP_URL` local | Presentes en `.env.local` |
| `SUPABASE_SERVICE_ROLE_KEY` | Ausente localmente |
| Env de GitHub App (`GITHUB_APP_*`, webhook secret) | Ausente localmente |
| Proyecto Vercel `hubforge` | Vinculado; dominio prod activo |
| Resend / Sentry | Opcional |
## Estado conocido (auditoría de staging)

Ejecutar localmente:
Esta tabla describe lo verificado en auditoría, no un estado de lanzamiento.

| Pieza | Estado conocido |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Código en `main` con `/api/ready` y sync de check runs | Integrado en el repositorio |
| Despliegue en `https://hubforge-six.vercel.app` | Desactualizado respecto a `main` (`GET /api/ready` responde 404) |
| `.env.local` en esta máquina de trabajo | Ausente — flags de auth locales en MISS |
| Env de Vercel Production (nombres) | No listado: CLI/MCP de Vercel sin autenticar en esta máquina |
| GitHub OAuth (Supabase Authentication → GitHub) | No verificado en dashboard — no modificar todavía |
| Redirect URLs de Auth | Deben incluir `http://localhost:3000/auth/callback` y `https://hubforge-six.vercel.app/auth/callback` — no verificado en dashboard |
| Migración `20260804120000_github_synced_check_runs.sql` | Presente en el repo; **aplicación remota no verificada** |
| Eventos GitHub App `Check run` / `Check suite` | Requeridos por el código; estado live de la App no verificado |

Nunca imprimas valores de secretos. Usa `pnpm verify:env` (nombres y OK/MISS).

```bash
pnpm verify:env
curl -s http://localhost:3000/api/ready
```

## 1. Auth (requerido para un MVP usable)
En producción, cuando el deploy esté al día:

```bash
curl -s https://hubforge-six.vercel.app/api/ready
```

1. Supabase → Authentication → Providers → GitHub habilitado con credenciales de OAuth App
## 1. Auth (login de usuarios — OAuth GitHub)

No confundir con la GitHub App.

1. Supabase → Authentication → Providers → GitHub habilitado con credenciales de **OAuth App** (no App de sincronización)
2. URL Configuration:
- Site URL: `https://hubforge-six.vercel.app`
- Redirect URLs incluyen:
- `http://localhost:3000/auth/callback`
- `https://hubforge-six.vercel.app/auth/callback`
3. `.env.local` / env de Vercel:
- `NEXT_PUBLIC_SUPABASE_URL=https://pnpkgfhpwvdkhbncfwqz.supabase.co`
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=…`
- `NEXT_PUBLIC_APP_URL=https://hubforge-six.vercel.app` (prod) o `http://localhost:3000` (local)
4. Verificar: iniciar sesión en `/login` → aterrizar en `/app`
3. Env local y Vercel (Production + Preview):
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (o fallback `NEXT_PUBLIC_SUPABASE_ANON_KEY`)
- `NEXT_PUBLIC_APP_URL` — origen público usado en `redirectTo` de OAuth
4. Verificar: `/login` muestra el botón de GitHub habilitado, sin avisos técnicos, y el flujo aterriza en `/app`

## 2. Sincronización con GitHub (requerido para la capacidad MVP completa #8)
`SUPABASE_SERVICE_ROLE_KEY` no es necesaria para el login; sí lo es para sync.

1. Crear GitHub App — `docs/operations/github-app-setup.md`
## 2. Sincronización con GitHub (GitHub App)

1. Crear/actualizar GitHub App — `docs/operations/github-app-setup.md`
2. Webhook URL: `https://hubforge-six.vercel.app/api/github/webhooks`
3. Setup URL: `https://hubforge-six.vercel.app/api/github/setup`
4. Configurar en `.env.local` **y** Vercel (Production + Preview):
- `SUPABASE_SERVICE_ROLE_KEY` (Supabase → Project Settings → API)
4. Eventos: Issues, Pull request, Push, Installation, Installation repositories, **Check run**, **Check suite**
5. Configurar en `.env.local` **y** Vercel (Production + Preview):
- `SUPABASE_SERVICE_ROLE_KEY`
- `GITHUB_APP_ID`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`
- `GITHUB_APP_PRIVATE_KEY` (PEM con escapes `\n` o secreto multilínea)
- `GITHUB_APP_PRIVATE_KEY`
- `GITHUB_APP_SLUG`, `GITHUB_WEBHOOK_SECRET`
5. Verificar: vincular `owner/repo` con installation id → **Sync now** rellena issues/PRs/commits
6. Base de datos: además de las migraciones anteriores, existe `supabase/migrations/20260804120000_github_synced_check_runs.sql`. **No afirmar que está aplicada en remoto hasta listar migraciones con la CLI autenticada.** No aplicar todavía desde esta fase.
7. Verificar: vincular `owner/repo` con installation id → **Sync now** rellena issues/PRs/commits (check runs son best-effort)

## 3. Opcional

Expand All @@ -54,13 +72,17 @@ curl -s http://localhost:3000/api/ready

## 4. Desplegar

1. Fusionar PR con el trabajo de preparación
2. Confirmar que el env de Vercel Production coincide con la lista anterior
No desplegar hasta terminar la auditoría de staging y tener Auth + env de producción listos.

1. Fusionar el trabajo de preparación cuando se autorice
2. Confirmar que el env de Vercel Production coincide con las variables de Auth (y de App si la sync entra en el lanzamiento)
3. Redesplegar si se añadió env después del último deploy
4. Smoke: `/` → `/login` → `/app` → crear tarea → enlace de invitación en Team → GitHub Sync now
4. Smoke: `/` → `/login` (copy pública, sin variables de entorno) → `/app` → crear tarea → enlace de invitación en Team → GitHub Sync now
5. `GET /api/ready` en producción debe devolver JSON con `authReady` (y `githubReady` si aplica)

## Listo cuando

- `pnpm verify:env` termina con código 0 para los flags de auth
- `/api/ready` devuelve `"authReady": true` en el entorno desplegado
- `/api/ready` en el entorno desplegado devuelve `"authReady": true`
- Los flags de sincronización con GitHub son true si la sync de repositorios está en el alcance del lanzamiento
- El login público no menciona Supabase, cookies, proxy ni nombres de variables
5 changes: 3 additions & 2 deletions docs/operations/supabase-auth-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ O pega estos archivos en el editor SQL, en orden:
5. `supabase/migrations/20260728240000_github_app_sync.sql`
6. `supabase/migrations/20260728250000_chat_realtime.sql`
7. `supabase/migrations/20260803200000_operations_history_deps_github_activity.sql`
8. `supabase/migrations/20260804120000_github_synced_check_runs.sql` (presente en el repo; aplicación remota no verificada en la auditoría de staging)

Consulta también `docs/operations/github-app-setup.md` y `docs/operations/production-checklist.md`.
Consulta también `docs/operations/github-app-setup.md` y `docs/operations/production-checklist.md`. No apliques migraciones remotas hasta que se autorice.

## 7. Verificar

Expand All @@ -88,4 +89,4 @@ Consulta también `docs/operations/github-app-setup.md` y `docs/operations/produ
5. Aterriza en `/app` con tu nombre en la cabecera
6. Sign out

Sin variables de entorno de Supabase, el botón de inicio de sesión permanece deshabilitado y `/app` redirige a `/login`. No hay workspace demo offline.
Sin Auth configurada, `/login` muestra un estado neutro (“no disponible temporalmente”), el botón de GitHub permanece deshabilitado y `/app` redirige a `/login`. La UI pública no debe mostrar nombres de variables ni detalles de Supabase. No hay workspace demo offline.
22 changes: 19 additions & 3 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,25 @@ test.describe("smoke", () => {
await expect(
page.getByRole("heading", { name: /Sign in|Iniciar sesión/i }),
).toBeVisible();
await expect(
page.getByRole("button", { name: /Continue with GitHub|Continuar con GitHub/i }),
).toBeVisible();
const github = page.getByRole("button", {
name: /Continue with GitHub|Continuar con GitHub/i,
});
await expect(github).toBeVisible();
const body = await page.locator("main").innerText();
expect(body).not.toMatch(
/NEXT_PUBLIC_SUPABASE|Supabase Auth|proxy de Next\.js|anon key/i,
);
if (!(await github.isEnabled())) {
await expect(
page
.getByRole("status")
.or(
page.getByText(
/Sign in is temporarily unavailable|El inicio de sesión no está disponible temporalmente/i,
),
),
).toBeVisible();
}
});

test("language switcher is available on landing", async ({ page }) => {
Expand Down
17 changes: 15 additions & 2 deletions src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { PUBLIC_SIGN_IN_ERROR } from "@/features/authentication/sign-in-errors";
import { safeRedirectPath } from "@/features/authentication/safe-redirect";

function signInErrorUrl(origin: string, next: string) {
const url = new URL("/login", origin);
url.searchParams.set("error", PUBLIC_SIGN_IN_ERROR);
if (next && next !== "/app") {
url.searchParams.set("next", next);
}
return url;
}

export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
Expand All @@ -10,7 +20,10 @@ export async function GET(request: Request) {
if (code) {
const supabase = await createSupabaseServerClient();
if (!supabase) {
return NextResponse.redirect(`${origin}/auth/error?reason=supabase-not-configured`);
if (process.env.NODE_ENV === "development") {
console.warn("Auth environment is incomplete.");
}
return NextResponse.redirect(signInErrorUrl(origin, next));
}

const { error } = await supabase.auth.exchangeCodeForSession(code);
Expand All @@ -28,5 +41,5 @@ export async function GET(request: Request) {
}
}

return NextResponse.redirect(`${origin}/auth/error?reason=auth-code`);
return NextResponse.redirect(signInErrorUrl(origin, next));
}
55 changes: 32 additions & 23 deletions src/app/auth/error/page.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
import Link from "next/link";
import { LanguageSwitcher } from "@/components/shared/language-switcher";
import { ThemeToggle } from "@/components/shared/theme-toggle";
import { buttonVariants } from "@/components/ui/button";
import { getDictionary, getLocale } from "@/i18n/get-dictionary";
import { cn } from "@/lib/utils";

export const metadata = {
title: "Authentication error",
title: "Sign-in failed",
};

const reasons: Record<string, string> = {
"auth-code": "We could not complete the GitHub sign-in callback.",
"oauth-start": "GitHub OAuth could not be started. Check Supabase provider settings.",
"supabase-not-configured":
"Supabase environment variables are missing. Copy .env.example to .env.local and configure your project.",
};

export default async function AuthErrorPage({
searchParams,
}: {
searchParams: Promise<{ reason?: string }>;
}) {
const params = await searchParams;
const reason = params.reason ?? "auth-code";
const message = reasons[reason] ?? reasons["auth-code"];
export default async function AuthErrorPage() {
const locale = await getLocale();
const t = await getDictionary(locale);

return (
<main className="mx-auto flex min-h-dvh w-full max-w-lg flex-col justify-center px-4 py-12">
<main className="mx-auto flex min-h-dvh w-full max-w-md flex-col justify-center px-4 py-12">
<div className="mb-8 flex items-center justify-between gap-2">
<Link href="/" className="t-display-sm text-[var(--hf-accent)]">
{t.common.brand}
</Link>
<div className="flex items-center gap-1">
<LanguageSwitcher
locale={locale}
labels={{
language: t.common.language,
english: t.common.english,
spanish: t.common.spanish,
}}
/>
<ThemeToggle />
</div>
</div>

<section className="panel grid gap-4 p-5">
<div className="grid gap-1">
<p className="t-body-sm inline-flex items-center gap-1.5 font-medium text-[var(--hf-error)]">
Error
<h1 className="t-display text-pretty text-[var(--hf-ink)]">
{t.login.failedTitle}
</h1>
<p role="alert" className="t-body text-[var(--hf-ink-muted)]">
{t.login.error}
</p>
<h1 className="t-display text-[var(--hf-ink)]">Sign-in failed</h1>
<p className="t-body text-[var(--hf-ink-muted)]">{message}</p>
</div>
<div className="flex flex-wrap gap-3">
<Link href="/login" className={cn(buttonVariants())}>
Back to sign in
{t.login.backToSignIn}
</Link>
<Link href="/" className={cn(buttonVariants({ variant: "outline" }))}>
Home
{t.login.home}
</Link>
</div>
</section>
Expand Down
43 changes: 14 additions & 29 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Link from "next/link";
import { LanguageSwitcher } from "@/components/shared/language-switcher";
import { ThemeToggle } from "@/components/shared/theme-toggle";
import { buttonVariants } from "@/components/ui/button";
import { LoginScreen } from "@/features/authentication/login-screen";
import { isPublicSignInError } from "@/features/authentication/sign-in-errors";
import { signInWithGitHub } from "@/features/authentication/actions";
import { safeRedirectPath } from "@/features/authentication/safe-redirect";
import { getDictionary, getLocale } from "@/i18n/get-dictionary";
import { isSupabaseConfigured } from "@/lib/supabase/config";
import { cn } from "@/lib/utils";

export const metadata = {
title: "Sign in",
Expand All @@ -22,7 +22,11 @@ export default async function LoginPage({
const t = await getDictionary(locale);
const configured = isSupabaseConfigured();
const next = safeRedirectPath(params.next);
const showConfigError = params.error === "supabase-not-configured" || !configured;
const showError = configured && isPublicSignInError(params.error);

if (!configured && process.env.NODE_ENV === "development") {
console.warn("Auth environment is incomplete.");
}

return (
<main className="mx-auto flex min-h-dvh w-full max-w-md flex-col justify-center px-4 py-12">
Expand All @@ -43,32 +47,13 @@ export default async function LoginPage({
</div>
</div>

<section className="panel grid gap-4 p-5">
<div className="grid gap-1">
<h1 className="t-display text-[var(--hf-ink)]">{t.login.title}</h1>
<p className="t-body text-[var(--hf-ink-muted)]">{t.login.body}</p>
</div>

{showConfigError ? (
<p
role="status"
className="t-body-sm rounded-[var(--radius-md)] bg-[var(--hf-caution-quiet)] px-3 py-2 text-[var(--hf-caution)]"
>
{t.login.configWarning}
</p>
) : null}

<form action={signInWithGitHub} className="grid gap-3">
<input type="hidden" name="next" value={next} />
<button
type="submit"
disabled={!configured}
className={cn(buttonVariants({ size: "lg" }), "w-full disabled:opacity-60")}
>
{t.login.continueGithub}
</button>
</form>
</section>
<LoginScreen
copy={t.login}
configured={configured}
showError={showError}
next={next}
action={signInWithGitHub}
/>
</main>
);
}
Loading