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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ JUDGE0_API_KEY=
# SUBMISSION_REPOSITORY_TOKEN because GitHub reserves the GITHUB_ prefix.
GITHUB_REPOSITORY_TOKEN=github_pat_REPLACE_ME
GITHUB_REPOSITORY_OWNER=eeminionn
GITHUB_REPOSITORY_NAME=tomatin-code-lab-resoluciones
29 changes: 28 additions & 1 deletion .github/workflows/supabase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
SUBMISSION_REPOSITORY_TOKEN: ${{ secrets.SUBMISSION_REPOSITORY_TOKEN }}
SUBMISSION_REPOSITORY_OWNER: ${{ vars.SUBMISSION_REPOSITORY_OWNER }}
SUBMISSION_REPOSITORY_NAME: ${{ vars.SUBMISSION_REPOSITORY_NAME }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -60,13 +61,39 @@ jobs:
- name: Apply database migrations
run: supabase db push

- name: Verify central submission repository
run: |
owner="${SUBMISSION_REPOSITORY_OWNER:-eeminionn}"
repository="${SUBMISSION_REPOSITORY_NAME:-tomatin-code-lab-resoluciones}"
status="$(
curl --silent --show-error \
--output repository.json \
--write-out "%{http_code}" \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer $SUBMISSION_REPOSITORY_TOKEN" \
--header "X-GitHub-Api-Version: 2026-03-10" \
"https://api.github.com/repos/$owner/$repository"
)"
test "$status" = "200" || {
echo "::error::The submission token cannot access $owner/$repository (HTTP $status)."
exit 1
}
jq --exit-status \
'.private == true and .permissions.push == true' \
repository.json >/dev/null || {
echo "::error::$owner/$repository must be private and writable by the submission token."
exit 1
}

- name: Configure repository sync
run: |
owner="${SUBMISSION_REPOSITORY_OWNER:-eeminionn}"
repository="${SUBMISSION_REPOSITORY_NAME:-tomatin-code-lab-resoluciones}"
supabase secrets set \
--project-ref "$PROJECT_ID" \
GITHUB_REPOSITORY_TOKEN="$SUBMISSION_REPOSITORY_TOKEN" \
GITHUB_REPOSITORY_OWNER="$owner"
GITHUB_REPOSITORY_OWNER="$owner" \
GITHUB_REPOSITORY_NAME="$repository"

- name: Deploy Edge Functions
run: supabase functions deploy --project-ref "$PROJECT_ID"
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ II con veinte misiones originales.
backend cuando está disponible.
- JavaScript en Web Worker, Python con Pyodide y ejecución remota con Judge0.
- Supabase Auth exclusivamente con GitHub, Postgres, Realtime, RLS,
invitaciones de un uso y roles `owner`, `mentor` y `student`.
invitaciones configurables y roles `owner`, `mentor` y `student`.
- Asignaciones por estudiante, revisión, reentrega, XP idempotente y ranking
basado únicamente en tareas aprobadas.
- Un repositorio privado por estudiante bajo `eeminionn`, con una carpeta por
misión y actualización automática al entregar.
- Un único repositorio privado de resoluciones bajo `eeminionn`, con carpetas
aisladas por estudiante y actualización automática al entregar.
- Panel del mentor con métricas reales, matriz filtrable, actividad reciente,
detalle por estudiante, historial de código guardado y vista estudiante de
solo lectura.
Expand Down Expand Up @@ -79,24 +79,36 @@ VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
a las Edge Functions. `JUDGE0_URL` y `JUDGE0_API_KEY` son opcionales; por
defecto se usa la instancia pública de Judge0 CE.

## Repositorios de estudiantes
## Entregas en GitHub

La aplicación crea un repositorio privado
`tomatin-code-lab-USUARIO-ID` bajo `eeminionn`, invita al estudiante como
colaborador y guarda cada entrega en:
La aplicación usa un único repositorio privado
`eeminionn/tomatin-code-lab-resoluciones` y guarda cada entrega en:

```text
misiones/slug-de-la-mision/solucion.js
misiones/slug-de-la-mision/solucion.py
misiones/slug-de-la-mision/solucion.cpp
resoluciones/usuario-id-corto/misiones/slug-de-la-mision/solucion.js
resoluciones/usuario-id-corto/misiones/slug-de-la-mision/solucion.py
resoluciones/usuario-id-corto/misiones/slug-de-la-mision/solucion.cpp
```

Los estudiantes no son colaboradores del repositorio central. GitHub concede
permisos por repositorio, no por carpeta; invitarlos permitiría que vieran
respuestas ajenas. El estudiante consulta su código, historial y estado de
sincronización desde el aula, mientras que el mentor puede abrir la carpeta
privada desde `/admin`. Los commits usan el correo `noreply` verificado del
estudiante para conservar la atribución cuando GitHub puede asociarla.

Para activarlo, crea un fine-grained personal access token de GitHub cuyo
resource owner sea `eeminionn`, con acceso a todos los repositorios y permisos
`Administration: Read and write` y `Contents: Read and write`. Guárdalo como el
secret de Actions `SUBMISSION_REPOSITORY_TOKEN` y vuelve a ejecutar `Deploy
Supabase`. La variable opcional `SUBMISSION_REPOSITORY_OWNER` permite cambiar
el propietario; si no existe, se usa `eeminionn`.
resource owner sea `eeminionn`, con acceso al repositorio central y permisos
`Administration: Read and write` y `Contents: Read and write`. Guárdalo como
el secret de Actions `SUBMISSION_REPOSITORY_TOKEN` y vuelve a ejecutar `Deploy
Supabase`. Las variables opcionales `SUBMISSION_REPOSITORY_OWNER` y
`SUBMISSION_REPOSITORY_NAME` permiten cambiar el propietario y el nombre; sus
valores por defecto son `eeminionn` y `tomatin-code-lab-resoluciones`.

Los repositorios individuales creados por versiones anteriores no se eliminan
automáticamente. La siguiente sesión o entrega migra el registro del estudiante
al almacenamiento central; los repositorios anteriores pueden archivarse
después de comprobar la copia y definir una política de conservación.

## Verificación

Expand Down
7 changes: 4 additions & 3 deletions docs/BETA_ROLLOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ versión estable de la raíz no se reemplaza todavía.
## Barreras automáticas

- CI ejecuta las pruebas heredadas, TypeScript, Edge Functions, las 60
soluciones de referencia, el build y nueve recorridos Playwright.
soluciones de referencia, el build y los recorridos Playwright.
- pgTAP aplica todas las migraciones desde cero y verifica RLS, privacidad y
contratos de versión.
- GitHub Pages falla si faltan las variables públicas de Supabase.
- Supabase falla si faltan los secretos de despliegue o de sincronización con
los repositorios estudiantiles.
el repositorio privado de resoluciones.
- Después de publicar, Pages comprueba que `/beta/` responda y sirva la versión
3.0 desde la ruta correcta.

Expand All @@ -23,7 +23,8 @@ versión estable de la raíz no se reemplaza todavía.
- [ ] Abrir la Misión 01 en dos acciones y comprenderla sin revelar pistas.
- [ ] Guardar código distinto en JavaScript, Python y C++ sin perder borradores.
- [ ] Ejecutar al menos un caso correcto y uno incorrecto en cada lenguaje.
- [ ] Entregar una misión y comprobar su actualización en el repositorio privado.
- [ ] Entregar una misión y comprobar que cada estudiante actualice únicamente
su carpeta dentro del repositorio privado central.
- [ ] Revisar desde `/admin`, comentar una línea y solicitar cambios.
- [ ] Confirmar que la reentrega usa la versión actual sin borrar el historial.
- [ ] Aprobar una entrega y comprobar que el XP se emita una sola vez.
Expand Down
38 changes: 38 additions & 0 deletions supabase/functions/_shared/github-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Language } from "./types.ts";

const FILE_NAMES: Record<Language, string> = {
javascript: "solucion.js",
python: "solucion.py",
cpp: "solucion.cpp",
};

function safeSegment(value: string, fallback: string): string {
return (
value
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[._-]+|[._-]+$/g, "")
.slice(0, 80) || fallback
);
}

export function studentSubmissionFolder(
githubLogin: string,
userId: string,
): string {
const login = safeSegment(githubLogin, "estudiante");
const shortId =
userId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 8) || "sin-id";
return `${login}-${shortId}`;
}

export function submissionFilePath(input: {
githubLogin: string;
userId: string;
missionSlug: string;
language: Language;
}): string {
const student = studentSubmissionFolder(input.githubLogin, input.userId);
const mission = safeSegment(input.missionSlug, "mision");
return `resoluciones/${student}/misiones/${mission}/${FILE_NAMES[input.language]}`;
}
Loading