diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2adccb1c..27720761 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -169,10 +169,13 @@ service Dockerfiles use BuildKit cache mounts. ./scripts/test.sh ./scripts/lint.sh ./scripts/format.sh -./scripts/mypy.sh +./scripts/typecheck.sh # Python mypy + dashboard TypeScript +./scripts/mypy.sh # Python-only typecheck +./scripts/check-all.sh ``` -For dashboard-only checks: +These top-level scripts include the admin dashboard where applicable. For +dashboard-only checks: ```bash cd apps/admin_dashboard diff --git a/README.md b/README.md index e93902ed..0ebd4350 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,9 @@ To run the Discord bot too: ./scripts/test.sh ./scripts/lint.sh ./scripts/format.sh -./scripts/mypy.sh +./scripts/typecheck.sh # Python mypy + dashboard TypeScript +./scripts/mypy.sh # Python-only typecheck +./scripts/check-all.sh ``` For workspace archival, stop host-run dev processes and Docker Compose together: diff --git a/apps/admin_dashboard/bunfig.toml b/apps/admin_dashboard/bunfig.toml index 30e0142d..62d319d1 100644 --- a/apps/admin_dashboard/bunfig.toml +++ b/apps/admin_dashboard/bunfig.toml @@ -1,2 +1,8 @@ [install] -minimumReleaseAge = 604800 # 7 days +# Seven days, in seconds. Bun filters newly published direct and transitive +# package versions during resolution. +minimumReleaseAge = 604800 +minimumReleaseAgeExcludes = ["@types/bun", "typescript"] + +# Isolated installs reduce accidental reliance on undeclared dependencies. +linker = "isolated" diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index c33080d6..aa6d513b 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,6 +1,8 @@ # syntax=docker/dockerfile:1.7 -# Pinned from ghcr.io/astral-sh/uv:python3.12-bookworm-slim (verified 2026-02-21). -FROM ghcr.io/astral-sh/uv@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 +# Python pinned from python:3.12-slim-bookworm and uv pinned from ghcr.io/astral-sh/uv:0.11.18 (verified 2026-06-03). +FROM ghcr.io/astral-sh/uv@sha256:78bc42400d77b0678ba95765305c826652ed5431f399257271dda681d0318f03 AS uv +FROM python@sha256:93ab4b7fa528b25124c97bcc755415e60eb671a86b4dbe0328df2fe2d1c1193d +COPY --from=uv /uv /uvx /usr/local/bin/ WORKDIR /app ENV UV_LINK_MODE=copy diff --git a/apps/discord_bot/Dockerfile b/apps/discord_bot/Dockerfile index a78882bd..e9f4c590 100644 --- a/apps/discord_bot/Dockerfile +++ b/apps/discord_bot/Dockerfile @@ -1,6 +1,8 @@ # syntax=docker/dockerfile:1.7 -# Pinned from ghcr.io/astral-sh/uv:python3.12-bookworm-slim (verified 2026-02-21). -FROM ghcr.io/astral-sh/uv@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 +# Python pinned from python:3.12-slim-bookworm and uv pinned from ghcr.io/astral-sh/uv:0.11.18 (verified 2026-06-03). +FROM ghcr.io/astral-sh/uv@sha256:78bc42400d77b0678ba95765305c826652ed5431f399257271dda681d0318f03 AS uv +FROM python@sha256:93ab4b7fa528b25124c97bcc755415e60eb671a86b4dbe0328df2fe2d1c1193d +COPY --from=uv /uv /uvx /usr/local/bin/ WORKDIR /app ENV UV_LINK_MODE=copy diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index 3588b1f0..ac3dcec4 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -1,6 +1,8 @@ # syntax=docker/dockerfile:1.7 -# Pinned from ghcr.io/astral-sh/uv:python3.12-bookworm-slim (verified 2026-02-21). -FROM ghcr.io/astral-sh/uv@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 +# Python pinned from python:3.12-slim-bookworm and uv pinned from ghcr.io/astral-sh/uv:0.11.18 (verified 2026-06-03). +FROM ghcr.io/astral-sh/uv@sha256:78bc42400d77b0678ba95765305c826652ed5431f399257271dda681d0318f03 AS uv +FROM python@sha256:93ab4b7fa528b25124c97bcc755415e60eb671a86b4dbe0328df2fe2d1c1193d +COPY --from=uv /uv /uvx /usr/local/bin/ WORKDIR /app ENV UV_LINK_MODE=copy diff --git a/scripts/check-all.sh b/scripts/check-all.sh index 552f074b..3b74c18c 100755 --- a/scripts/check-all.sh +++ b/scripts/check-all.sh @@ -1,16 +1,45 @@ -#!/bin/bash -set -e +#!/usr/bin/env sh +set -eu echo "Running all checks..." echo -./scripts/format.sh +./scripts/lint.sh echo -./scripts/lint.sh +echo "Checking Python formatting..." +uv run ruff format --check apps/api/src/five08 apps/discord_bot/src/five08 apps/worker/src/five08 packages/shared/src/five08 tests echo ./scripts/mypy.sh echo -echo "✅ All checks passed!" \ No newline at end of file +./scripts/test.sh +echo + +echo "Building admin dashboard..." +dashboard_static_dir="apps/api/src/five08/backend/static/dashboard" +dashboard_build_dir=$(mktemp -d "${TMPDIR:-/tmp}/five08-dashboard-build.XXXXXX") +cleanup_dashboard_build_dir() { + rm -rf "$dashboard_build_dir" +} +trap cleanup_dashboard_build_dir EXIT HUP INT TERM +( + cd apps/admin_dashboard + bun run build -- --outDir "$dashboard_build_dir" +) +if ! diff_output=$(diff -qr "$dashboard_static_dir" "$dashboard_build_dir"); then + echo + echo "Dashboard build output is stale. Run 'cd apps/admin_dashboard && bun run build' and commit the generated static assets." + echo "$diff_output" + exit 1 +fi +if [ -n "$(git status --porcelain -- "$dashboard_static_dir")" ]; then + echo + echo "Dashboard build output is stale. Run 'cd apps/admin_dashboard && bun run build' and commit the generated static assets." + git status --short -- "$dashboard_static_dir" + exit 1 +fi +echo + +echo "All checks passed." diff --git a/scripts/dev.sh b/scripts/dev.sh index 1461e6a3..93a9f0a9 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -5,6 +5,9 @@ script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) . "$script_dir/worktree-env.sh" worktree_env_load "$script_dir" +UV_BIN=${UV_BIN:-$(command -v uv)} +export UV_BIN + # dev.sh owns host-run service URLs so every launched process shares the same # worktree-local infra and app ports. export REDIS_URL="redis://127.0.0.1:${REDIS_HOST_PORT}/0" @@ -41,7 +44,7 @@ start_infra() { } run_migrations() { - uv run --package worker python3 -c 'from five08.worker.db_migrations import run_job_migrations; run_job_migrations()' + "$UV_BIN" run --package worker python3 -c 'from five08.worker.db_migrations import run_job_migrations; run_job_migrations()' } reclaim_service_port() { @@ -220,7 +223,7 @@ EOF web|api) reclaim_service_port web run_migrations - exec uv run --package api uvicorn five08.backend.api:create_app \ + exec "$UV_BIN" run --package api uvicorn five08.backend.api:create_app \ --factory \ --host "${WEB_HOST:-${WEBHOOK_INGEST_HOST:-0.0.0.0}}" \ --port "$WEB_PORT" \ @@ -231,11 +234,12 @@ EOF ;; worker) run_migrations - exec uv run watchfiles \ + worker_command="$(shell_quote "$UV_BIN") run --package worker worker-consumer" + exec "$UV_BIN" run watchfiles \ --filter python \ --sigint-timeout 5 \ --sigkill-timeout 10 \ - 'uv run --package worker worker-consumer' \ + "$worker_command" \ apps/worker/src \ packages/shared/src ;; diff --git a/scripts/dev_mux.py b/scripts/dev_mux.py index a3adf50e..f9f583c7 100755 --- a/scripts/dev_mux.py +++ b/scripts/dev_mux.py @@ -4,6 +4,8 @@ from __future__ import annotations import os +import shlex +import shutil import signal import socket import subprocess @@ -33,11 +35,13 @@ def _service_commands( env: dict[str, str], selected_services: set[str] | None = None ) -> list[tuple[str, list[str]]]: selected_services = selected_services or set(ALL_SERVICES) + uv_bin = env.get("UV_BIN") or shutil.which("uv") or "uv" + uv_command = shlex.quote(uv_bin) commands = [ ( "web", [ - "uv", + uv_bin, "run", "--package", "api", @@ -60,7 +64,7 @@ def _service_commands( ( "worker", [ - "uv", + uv_bin, "run", "watchfiles", "--filter", @@ -69,7 +73,7 @@ def _service_commands( "5", "--sigkill-timeout", "10", - "uv run --package worker worker-consumer", + f"{uv_command} run --package worker worker-consumer", "apps/worker/src", "packages/shared/src", ], @@ -77,7 +81,7 @@ def _service_commands( ( "discord-bot", [ - "uv", + uv_bin, "run", "watchfiles", "--filter", @@ -86,7 +90,7 @@ def _service_commands( "5", "--sigkill-timeout", "10", - "uv run --package discord_bot discord-bot", + f"{uv_command} run --package discord_bot discord-bot", "apps/discord_bot/src", "packages/shared/src", ], diff --git a/scripts/format.sh b/scripts/format.sh index cc21748b..d18d59d3 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,5 +1,12 @@ -#!/bin/bash -set -e +#!/usr/bin/env sh +set -eu echo "Running ruff format..." -uv run ruff format apps/api/src/five08/ apps/discord_bot/src/five08/ apps/worker/src/five08/ packages/shared/src/five08/ tests/ +uv run ruff format apps/api/src/five08 apps/discord_bot/src/five08 apps/worker/src/five08 packages/shared/src/five08 tests + +echo +echo "Running admin dashboard format..." +( + cd apps/admin_dashboard + bun run format +) diff --git a/scripts/lint.sh b/scripts/lint.sh index ace154f7..78693af3 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,5 +1,12 @@ -#!/bin/bash -set -e +#!/usr/bin/env sh +set -eu echo "Running ruff check..." -uv run ruff check apps/api/src/five08/ apps/discord_bot/src/five08/ apps/worker/src/five08/ packages/shared/src/five08/ tests/ +uv run ruff check apps/api/src/five08 apps/discord_bot/src/five08 apps/worker/src/five08 packages/shared/src/five08 tests + +echo +echo "Running admin dashboard lint..." +( + cd apps/admin_dashboard + bun run lint +) diff --git a/scripts/mypy.sh b/scripts/mypy.sh index 0936b53c..40fcfa5b 100755 --- a/scripts/mypy.sh +++ b/scripts/mypy.sh @@ -1,5 +1,5 @@ -#!/bin/bash -set -e +#!/usr/bin/env sh +set -eu echo "Running mypy..." uv run mypy apps/api/src/five08/ apps/discord_bot/src/five08/ apps/worker/src/five08/ packages/shared/src/five08/ --ignore-missing-imports --explicit-package-bases diff --git a/scripts/test.sh b/scripts/test.sh index ebc7b4c3..13405961 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,5 +1,12 @@ -#!/bin/bash -set -e +#!/usr/bin/env sh +set -eu echo "Running tests..." -uv run pytest tests/ -v --tb=short \ No newline at end of file +uv run pytest tests/ -v --tb=short + +echo +echo "Running admin dashboard tests..." +( + cd apps/admin_dashboard + bun run test +) diff --git a/scripts/typecheck.sh b/scripts/typecheck.sh new file mode 100755 index 00000000..a5fd856b --- /dev/null +++ b/scripts/typecheck.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -eu + +./scripts/mypy.sh + +echo +echo "Running admin dashboard typecheck..." +( + cd apps/admin_dashboard + bun run typecheck +) diff --git a/tests/integration/test_dashboard_playwright.py b/tests/integration/test_dashboard_playwright.py index 795ed4cf..9e520e8d 100644 --- a/tests/integration/test_dashboard_playwright.py +++ b/tests/integration/test_dashboard_playwright.py @@ -427,12 +427,21 @@ def gig_application_status_route(route: Any) -> None: gig_application_requested.set() body = route.request.post_data_json assert body["status"] == "unavailable" + casey_application_id = "22222222-2222-4222-8222-222222222222" + for application in gigs_list_payload[0]["applications"]: + if application["id"] == casey_application_id: + application["status"] = "unavailable" + break + else: + raise AssertionError( + f"Expected Casey Candidate application fixture {casey_application_id}" + ) route.fulfill( status=200, content_type="application/json", body=json.dumps( { - "id": "22222222-2222-4222-8222-222222222222", + "id": casey_application_id, "status": "unavailable", } ), @@ -627,10 +636,23 @@ def sync_route(route: Any) -> None: page.get_by_role("button", name="Add candidate").click() assert gig_application_add_requested.wait(timeout=5) page.get_by_text("Devon Candidate").wait_for() - page.get_by_label("Candidate status for Casey Candidate").select_option( - "unavailable" - ) + casey_status = page.get_by_label("Candidate status for Casey Candidate") + expect(casey_status).to_be_enabled() + expect(casey_status).to_have_value("suggested") + with page.expect_request( + lambda request: ( + request.method == "POST" + and request.url.endswith( + "/dashboard/api/gigs/" + "11111111-1111-4111-8111-111111111111" + "/applications/" + "22222222-2222-4222-8222-222222222222/status" + ) + ) + ): + casey_status.select_option("unavailable") assert gig_application_requested.wait(timeout=5) + expect(casey_status).to_have_value("unavailable") assert gig_detail_requests gigs_list_payload = []