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
2 changes: 2 additions & 0 deletions .github/scripts/bump-app-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ echo "Computed APP_VERSION=${APP_VERSION} (latest tag: ${LATEST_TAG:-none})"

if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "APP_VERSION=${APP_VERSION}" >>"$GITHUB_ENV"
# Keep Sentry release aligned with APP_VERSION so GitHub Fixes resolves in the right version.
echo "SENTRY_RELEASE=${APP_VERSION}" >>"$GITHUB_ENV"
fi
96 changes: 96 additions & 0 deletions .github/scripts/sync-sentry-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Finalize the Sentry release for APP_VERSION and resolve issues referenced via
# "Fixes WEBSITE-XX" in the merge commit message (inRelease = APP_VERSION).
#
# Requires SENTRY_ISSUE_AUTH_TOKEN (user/org token with event:write + org:read).
# The Vercel integration token cannot update issues.
set -euo pipefail

APP_VERSION="${APP_VERSION:?APP_VERSION is required}"
ORG="${SENTRY_ORG:-raidhub}"
PROJECT="${SENTRY_PROJECT:-website}"
TOKEN="${SENTRY_ISSUE_AUTH_TOKEN:-}"

if [[ -z "$TOKEN" ]]; then
echo "SENTRY_ISSUE_AUTH_TOKEN not set — skipping Sentry issue resolution."
exit 0
fi

COMMIT_SHA="${GITHUB_SHA:-}"
COMMIT_MSG=""
if [[ -n "$COMMIT_SHA" ]]; then
COMMIT_MSG="$(git log -1 --format=%B "$COMMIT_SHA" 2>/dev/null || true)"
fi

api() {
local method="$1"
local path="$2"
local body="${3:-}"
local url="https://sentry.io/api/0${path}"
if [[ -n "$body" ]]; then
curl -sS -f -X "$method" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$body" \
"$url"
else
curl -sS -f -X "$method" \
-H "Authorization: Bearer ${TOKEN}" \
"$url"
fi
}

echo "Syncing Sentry release ${APP_VERSION} for ${ORG}/${PROJECT}..."

# Ensure release exists and mark production deploy (idempotent).
api POST "/organizations/${ORG}/releases/" \
"{\"version\":\"${APP_VERSION}\",\"projects\":[\"${PROJECT}\"]}" \
2>/dev/null || true

api POST "/organizations/${ORG}/releases/${APP_VERSION}/deploys/" \
'{"environment":"production"}' \
2>/dev/null || true

api PUT "/organizations/${ORG}/releases/${APP_VERSION}/" \
"{\"dateReleased\":\"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\"}" \
2>/dev/null || true

if [[ -z "$COMMIT_MSG" ]]; then
echo "No commit message — release synced, skipping issue resolution."
exit 0
fi

mapfile -t ISSUE_SHORT_IDS < <(echo "$COMMIT_MSG" | grep -oE 'WEBSITE-[0-9A-Z]+' | sort -u)

if [[ ${#ISSUE_SHORT_IDS[@]} -eq 0 ]]; then
echo "No Fixes WEBSITE-* references in commit — release synced only."
exit 0
fi

echo "Resolving ${#ISSUE_SHORT_IDS[@]} issue(s) in release ${APP_VERSION}..."

for short_id in "${ISSUE_SHORT_IDS[@]}"; do
encoded_query="$(python3 -c "import urllib.parse; print(urllib.parse.quote('issue:${short_id}'))")"
issue_json="$(api GET "/projects/${ORG}/${PROJECT}/issues/?query=${encoded_query}&limit=1" || echo '[]')"
numeric_id="$(echo "$issue_json" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data:
if item.get('shortId') == '${short_id}':
print(item['id'])
break
" 2>/dev/null || true)"

if [[ -z "$numeric_id" ]]; then
echo " ${short_id}: not found, skipping"
continue
fi

api PUT "/issues/${numeric_id}/" \
"{\"status\":\"resolved\",\"statusDetails\":{\"inRelease\":\"${APP_VERSION}\"}}" \
>/dev/null

echo " ${short_id} -> resolved in ${APP_VERSION}"
done

echo "Sentry sync complete."
11 changes: 11 additions & 0 deletions .github/workflows/deploy-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
run: |
vercel env rm APP_VERSION production --yes --token=${{ secrets.VERCEL_TOKEN }}
echo "$APP_VERSION" | vercel env add APP_VERSION production --token=${{ secrets.VERCEL_TOKEN }}
vercel env rm SENTRY_RELEASE production --yes --token=${{ secrets.VERCEL_TOKEN }} || true
echo "$SENTRY_RELEASE" | vercel env add SENTRY_RELEASE production --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy Project to Vercel
shell: bash
Expand Down Expand Up @@ -100,3 +102,12 @@ jobs:
"${{ github.sha }}" \
"${{ secrets.GITHUB_TOKEN }}" \
"${{ github.repository }}"

- name: Sync Sentry release and resolve issues
env:
SENTRY_ISSUE_AUTH_TOKEN: ${{ secrets.SENTRY_ISSUE_AUTH_TOKEN }}
SENTRY_ORG: raidhub
SENTRY_PROJECT: website
run: |
chmod +x ./.github/scripts/sync-sentry-release.sh
./.github/scripts/sync-sentry-release.sh
3 changes: 2 additions & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ AWS_S3_REGION="us-east-1"
# SENTRY_DSN=""
# NEXT_PUBLIC_SENTRY_DSN=""
# SENTRY_ENVIRONMENT="development"
# SENTRY_RELEASE=""
# SENTRY_RELEASE="" # Set automatically in prod deploy; mirrors APP_VERSION
# SENTRY_ISSUE_AUTH_TOKEN="" # GitHub Actions secret — resolves Fixes WEBSITE-* issues post-deploy
# SENTRY_ORG="raidhub"
# SENTRY_PROJECT="website"
# SENTRY_AUTH_TOKEN="" # Required on Vercel production builds for client source map upload
Expand Down
5 changes: 4 additions & 1 deletion routing/reserved-vanity-slugs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"faq",
"leaderboards",
"multi",
"pgcr"
"pgcr",
"privacy",
"profile",
"terms"
]
21 changes: 18 additions & 3 deletions scripts/check-vanity-slugs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,26 @@ const urlPrefixes = readdirSync(appDir, { withFileTypes: true })
isTrackedAppRoute(entry.name)
)
.map(entry => entry.name)
.sort()

const routeGroupUrlPrefixes = readdirSync(appDir, { withFileTypes: true })
.filter(entry => entry.isDirectory() && entry.name.startsWith("("))
.flatMap(groupEntry => {
const groupDir = join(appDir, groupEntry.name)
return readdirSync(groupDir, { withFileTypes: true })
.filter(
entry =>
entry.isDirectory() &&
!entry.name.startsWith("[") &&
existsSync(join(groupDir, entry.name, "page.tsx"))
)
.map(entry => entry.name)
})

const allUrlPrefixes = [...new Set([...urlPrefixes, ...routeGroupUrlPrefixes])].sort()

const reservedSet = new Set(reserved)
const missingFromReserved = urlPrefixes.filter(slug => !reservedSet.has(slug))
const staleReserved = reserved.filter(slug => !urlPrefixes.includes(slug))
const missingFromReserved = allUrlPrefixes.filter(slug => !reservedSet.has(slug))
const staleReserved = reserved.filter(slug => !allUrlPrefixes.includes(slug))

const configSource = readFileSync(configPath, "utf8")
const reservedSorted = [...reserved].sort()
Expand Down
6 changes: 6 additions & 0 deletions src/app/(profile)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { notFound } from "next/navigation"

/** Bare /profile has no player id — avoid vanity rewrite to /user/profile. */
export default function ProfileIndexPage() {
notFound()
}
4 changes: 2 additions & 2 deletions src/app/calendar/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NotFound from "~/app/not-found"
import { notFound } from "next/navigation"

/** Reserved path — without this page, /calendar is rewritten to /user/calendar and 404s via vanity lookup. */
export default function CalendarPage() {
return <NotFound />
notFound()
}
4 changes: 2 additions & 2 deletions src/app/checkpoints/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NotFound from "~/app/not-found"
import { notFound } from "next/navigation"

/** Reserved path — without this page, /checkpoints is rewritten to /user/checkpoints and 404s via vanity lookup. */
export default function CheckpointsPage() {
return <NotFound />
notFound()
}
4 changes: 2 additions & 2 deletions src/app/leaderboards/LeaderboardSSR.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NotFound from "~/app/not-found"
import { notFound } from "next/navigation"
import { RaidHubError } from "~/services/raidhub/RaidHubError"
import { getRaidHubApi } from "~/services/raidhub/common"
import {
Expand Down Expand Up @@ -37,7 +37,7 @@ export const LeaderboardSSR = async <T extends RaidHubLeaderboardURL>(props: {
: null

if (ssrData === "not-found") {
return <NotFound />
notFound()
}

return (
Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboards/individual/[raid]/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import NotFound from "~/app/not-found"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { baseMetadata } from "~/lib/metadata"
import { prefetchManifest } from "~/services/raidhub/prefetchRaidHubManifest"
Expand Down Expand Up @@ -59,7 +59,7 @@ export default async function Page({ params, searchParams }: DynamicParams) {
const manifest = await prefetchManifest()
const definition = tryGetRaidDefinition(params.raid, manifest)
if (!definition) {
return <NotFound />
notFound()
}
const categoryName = getCategoryName(params.category)

Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboards/individual/global/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Metadata } from "next"
import NotFound from "~/app/not-found"
import { notFound } from "next/navigation"
import { CloudflareStaticImage } from "~/components/CloudflareImage"
import { baseMetadata } from "~/lib/metadata"
import { type PathParamsForLeaderboardURL } from "~/services/raidhub/types"
Expand Down Expand Up @@ -59,7 +59,7 @@ const ENTRIES_PER_PAGE = 50
export default async function Page({ params, searchParams }: DynamicParams) {
const categoryName = tryGetCategoryTitle(params.category)
if (!categoryName) {
return <NotFound />
notFound()
}

const apiParams = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import NotFound from "~/app/not-found"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { findPantheonVersionByPath } from "~/lib/manifest/pantheon"
import { baseMetadata } from "~/lib/metadata"
Expand Down Expand Up @@ -91,7 +91,7 @@ export default async function Page({
const manifest = await prefetchManifest()
const definitions = getDefinitions(params, manifest)
if (!definitions) {
return <NotFound />
notFound()
}

const { definition, activity, categoryName } = definitions
Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboards/team/[raid]/first/[version]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import NotFound from "~/app/not-found"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { findPantheonVersionByPath } from "~/lib/manifest/pantheon"
import { baseMetadata } from "~/lib/metadata"
Expand Down Expand Up @@ -97,7 +97,7 @@ export default async function Page({ params, searchParams }: DynamicParams) {
const manifest = await prefetchManifest()
const definitions = getDefinitions(params, manifest)
if (!definitions) {
return <NotFound />
notFound()
}

const { version, activity, isPantheon } = definitions
Expand Down
6 changes: 3 additions & 3 deletions src/app/leaderboards/team/[raid]/speedrun/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { Leaderboard } from "~/app/leaderboards/Leaderboard"
import NotFound from "~/app/not-found"
import { baseMetadata } from "~/lib/metadata"
import { SpeedrunVariables, type RTABoardCategory } from "~/lib/speedrun/speedrun-com-mappings"
import { prefetchManifest } from "~/services/raidhub/prefetchRaidHubManifest"
Expand Down Expand Up @@ -92,13 +92,13 @@ export default async function Page({ params }: DynamicParams) {

const raid = tryGetRaidDefinition(params.raid, manifest)
if (!raid) {
return <NotFound />
notFound()
}
const category = params.category === "all" ? undefined : params.category

const categoryId = SpeedrunVariables[raid.path]?.categoryId
if (!categoryId) {
return <NotFound />
notFound()
}

return (
Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboards/team/[raid]/worldfirst/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import NotFound from "~/app/not-found"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { baseMetadata } from "~/lib/metadata"
import { prefetchManifest } from "~/services/raidhub/prefetchRaidHubManifest"
Expand Down Expand Up @@ -51,7 +51,7 @@ export default async function Page({ params, searchParams }: DynamicParams) {
const manifest = await prefetchManifest()
const definition = tryGetRaidDefinition(params.raid, manifest)
if (!definition) {
return <NotFound />
notFound()
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import NotFound from "~/app/not-found"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { getActivePantheonIds, PANTHEON_COMMUNITY_RACE_VERSION_ID } from "~/lib/manifest/pantheon"
import { baseMetadata } from "~/lib/metadata"
Expand Down Expand Up @@ -66,7 +66,7 @@ export default async function Page({ searchParams }: { searchParams: Record<stri
const definitions = tryGetCommunityRaceDefinitions(manifest)

if (!definitions) {
return <NotFound />
notFound()
}

const { activity, version } = definitions
Expand Down
Loading
Loading