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
86 changes: 43 additions & 43 deletions .forgejo/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Build and Push Container Images

on:
pull_request:
push:
branches:
- main
Expand Down Expand Up @@ -36,6 +37,39 @@ env:
INFISICAL_MACHINE_IDENTITY_ID: ${{ vars.INFISICAL_MACHINE_IDENTITY_ID }}

jobs:
verify:
# Legacy Docker-backed Forgejo runner. See the backend delivery runbook.
runs-on: personal
container:
image: node:26.5.0-bookworm
options: --user 0
permissions:
contents: read
services:
postgres:
image: postgres:18.4-bookworm
env:
POSTGRES_DB: canvas_test
POSTGRES_USER: canvas_test
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd "pg_isready -U canvas_test"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
CANVAS_TEST_DATABASE_URL: postgresql://canvas_test@postgres:5432/canvas_test
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm exec playwright install --with-deps chromium
Comment thread
beastyrabbit marked this conversation as resolved.
- run: bash scripts/verify.sh

changes:
runs-on: personal
if: github.ref == 'refs/heads/main'
Expand All @@ -61,25 +95,7 @@ jobs:
base="$(git rev-list --max-parents=0 "$head")"
fi

changed="$(git diff --name-only "$base" "$head" || git diff --name-only HEAD~1 HEAD || true)"

has_changed() {
printf '%s\n' "$changed" | grep -Eq "$1"
}

set_output() {
name="$1"
pattern="$2"
if has_changed "$pattern"; then
echo "$name=true" >> "$GITHUB_OUTPUT"
else
echo "$name=false" >> "$GITHUB_OUTPUT"
fi
}

set_output frontend '^(app|components|lib|public)/|^(\.dockerignore|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|next\.config\.mjs|postcss\.config\.mjs|tailwind\.config\.ts|tsconfig\.json|biome\.json|components\.json|Dockerfile|docker-entrypoint\.sh)$'
set_output convex '^convex/|^convex\.json$'
set_output stream_canvas '^backend/stream-canvas/|^\.dockerignore$'
git diff --name-only "$base" "$head" | bash scripts/delivery-changes.sh >> "$GITHUB_OUTPUT"

tag-convex-changes:
runs-on: personal
Expand All @@ -97,31 +113,15 @@ jobs:
run: |
set -euo pipefail

previous_tag=$(
git tag --sort=-version:refname \
| grep -Fxv "$CURRENT_TAG" \
| head -n 1
)

if [ -z "$previous_tag" ]; then
echo "No previous release tag found; deploying Convex by default."
echo "convex=true" >> "$GITHUB_OUTPUT"
exit 0
fi

if git diff --quiet "$previous_tag..$CURRENT_SHA" -- convex; then
echo "convex=false" >> "$GITHUB_OUTPUT"
else
echo "convex=true" >> "$GITHUB_OUTPUT"
fi
bash scripts/delivery-changes.sh tag >> "$GITHUB_OUTPUT"

deploy-convex:
needs: [changes, tag-convex-changes]
needs: [verify, changes, tag-convex-changes]
if: |
always() &&
always() && needs.verify.result == 'success' && github.event_name != 'pull_request' &&
((github.ref == 'refs/heads/main' && needs.changes.outputs.convex == 'true') ||
(startsWith(github.ref, 'refs/tags/v') && needs.tag-convex-changes.outputs.convex == 'true') ||
github.event.inputs.deploy_convex == 'true')
(github.ref == 'refs/heads/main' && github.event.inputs.deploy_convex == 'true'))
runs-on: personal
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -192,9 +192,9 @@ jobs:
CLERK_JWT_ISSUER_DOMAIN: ${{ steps.ci-secrets.outputs.CLERK_JWT_ISSUER_DOMAIN }}

build-frontend:
needs: changes
needs: [verify, changes]
if: |
always() &&
always() && needs.verify.result == 'success' && github.event_name != 'pull_request' &&
(startsWith(github.ref, 'refs/tags/v') ||
needs.changes.outputs.frontend == 'true' ||
github.event.inputs.build_frontend == 'true')
Expand Down Expand Up @@ -290,9 +290,9 @@ jobs:
NEXT_PUBLIC_TLDRAW_LICENSE_KEY=${{ steps.ci-secrets.outputs.NEXT_PUBLIC_TLDRAW_LICENSE_KEY }}

build-stream-canvas:
needs: [changes, build-frontend]
needs: [verify, changes]
if: |
always() &&
always() && needs.verify.result == 'success' && github.event_name != 'pull_request' &&
(startsWith(github.ref, 'refs/tags/v') ||
needs.changes.outputs.stream_canvas == 'true' ||
github.event.inputs.build_stream_canvas == 'true')
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Release container images

on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read

jobs:
verify:
uses: ./.github/workflows/verify.yml

deploy-convex:
needs: verify
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
runs-on: arc-moddrop
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm exec convex deploy
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
CLERK_JWT_ISSUER_DOMAIN: ${{ secrets.CLERK_JWT_ISSUER_DOMAIN }}

build:
needs: [verify, deploy-convex]
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main'
runs-on: arc-moddrop
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- image: moddrop-frontend
dockerfile: Dockerfile
- image: moddrop-stream-canvas
dockerfile: backend/stream-canvas/Dockerfile
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
with:
driver: remote
endpoint: tcp://buildkitd.arc-runners.svc.cluster.local:1234
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v6
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=sha,prefix=
- uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
CONVEX_DEPLOYMENT=${{ secrets.CONVEX_DEPLOYMENT }}
NEXT_PUBLIC_TLDRAW_LICENSE_KEY=${{ secrets.NEXT_PUBLIC_TLDRAW_LICENSE_KEY }}
37 changes: 37 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Verify
on:
pull_request:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: arc-moddrop
container:
image: node:26.5.0-bookworm
options: --user 0
services:
postgres:
image: postgres:18.4-bookworm
env:
POSTGRES_DB: canvas_test
POSTGRES_USER: canvas_test
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd "pg_isready -U canvas_test"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
CANVAS_TEST_DATABASE_URL: postgresql://canvas_test@postgres:5432/canvas_test
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm exec playwright install --with-deps chromium
- run: bash scripts/verify.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ backend/stream-canvas/node_modules
backend/stream-canvas/data
backend/stream-canvas/bun.lockb
coverage
playwright-report
test-results
dist
build
out
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Clerk restricts the production instance to `moddrop.live`; it requires the
matching `pk_test_` / `sk_test_` pair instead. If needed, authenticate once with
`infisical login --domain http://192.168.60.11:8080`. Set
`MODDROP_DEV_DATABASE_URL` only when intentionally using a different
development PostgreSQL instance.
development PostgreSQL instance. With that override, startup and shutdown leave
the default Compose database alone. The default database port is loopback-only.

- `https://moddrop.localhost:1355`
- `https://moddrop-stream-canvas.localhost:1355`
Expand All @@ -46,6 +47,10 @@ the data model, migration flow, and production storage design.
## Check

```bash
pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build
pnpm --dir backend/stream-canvas run typecheck && pnpm --dir backend/stream-canvas run test
CANVAS_TEST_DATABASE_URL=<isolated-test-database-url> bash scripts/verify.sh
```

Use the pinned Node/pnpm versions and a disposable PostgreSQL database, never an
application database. The canonical script checks formatting, lint, application
and test types, unit/integration tests, builds, compiled runtime configuration,
and browser flows. See the backend guide's verification section for setup.
42 changes: 39 additions & 3 deletions app/app/rooms/[roomId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,67 @@ export default function StreamCanvasRoomPage() {
const [twitchChannel, setTwitchChannel] = useState<string | null>(null);
const [youtubePolicy, setYouTubePolicy] =
useState<YouTubePolicy>("preview_only");
const [loadedRoomId, setLoadedRoomId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [attempt, setAttempt] = useState(0);

// Fetch this room's Twitch channel from the accessible rooms list
// biome-ignore lint/correctness/useExhaustiveDependencies: attempt explicitly retries the same request
useEffect(() => {
if (!isLoaded || !isSignedIn) return;
let cancelled = false;
setError(null);
getAccessibleRooms(getToken)
.then((rooms) => {
const room = rooms.find((r) => r.id === roomId);
if (!cancelled && room) {
if (!room)
throw new Error(
"This room is unavailable or you no longer have access.",
);
if (!cancelled) {
setTwitchChannel(room.twitchChannel);
setYouTubePolicy(room.youtubePolicy);
setLoadedRoomId(roomId);
}
})
.catch((err) => {
if (!cancelled)
console.error("[stream-canvas] Failed to load room info:", err);
setError(err instanceof Error ? err.message : "Failed to load room");
});
return () => {
cancelled = true;
};
}, [getToken, isLoaded, isSignedIn, roomId]);
}, [getToken, isLoaded, isSignedIn, roomId, attempt]);

if (isLoaded && !isSignedIn)
return <p className="p-6">Sign in to open this room.</p>;
if (error)
return (
<div
role="alert"
className="flex h-screen flex-col items-center justify-center gap-4 p-6 text-center"
>
<p>{error}</p>
<button
type="button"
className="rounded border px-4 py-2"
onClick={() => setAttempt((value) => value + 1)}
>
Retry room connection
</button>
</div>
);
if (loadedRoomId !== roomId)
return (
<p role="status" className="p-6">
Loading room…
</p>
);

return (
<div className="fixed inset-0 overflow-hidden">
<CanvasEditor
key={roomId}
roomId={roomId}
twitchChannel={twitchChannel}
youtubePolicy={youtubePolicy}
Expand Down
Loading