You've already forked AstralRinth
forked from xxxOFFxxx/AstralRinth
Worker migration (#5072)
* Worker migration * Deploy on pnpm changes * Specify package manager * Manually bump Wrangler to 4.54 * Get rid of useless Wranglers worker * I take it back * Set account ID * Fix preview alias * feat: use workers api key * feat: try fix * fix: missing imports * fix: again * fix: only run push workflow on main or prod * feat: remove store id? * Populate secret store IDs * Use correct key name * Fix setting PREVIEW variable * Inject variables from wrangler into shell * Inject variables from wrangler into shell * Add git- prefix to preview-alias * No need to use environments now * fix: remove test as it's covered by staging deploy --------- Co-authored-by: Michael H. <michael@iptables.sh>
This commit is contained in:
98
.github/workflows/frontend-deploy.yml
vendored
Normal file
98
.github/workflows/frontend-deploy.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Deploy frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- prod
|
||||
paths:
|
||||
- 'apps/frontend/**/*'
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- '**/wrangler.jsonc'
|
||||
- '**/pnpm-*.yaml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Configure environment
|
||||
id: meta
|
||||
run: |
|
||||
echo "cmd=deploy" >> $GITHUB_OUTPUT
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
echo "url=https://staging.modrinth.com" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.ref }}" != "refs/heads/prod" ] && [ "${{ github.ref }}" != "refs/heads/main" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
echo "url=https://git-${GITHUB_SHA::8}-frontend-staging.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-${GITHUB_SHA::8} --var PREVIEW:true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Production env should be empty
|
||||
echo "url=https://modrinth.com" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm install
|
||||
|
||||
- name: Inject build variables
|
||||
working-directory: ./apps/frontend
|
||||
run: |
|
||||
if [ "${{ steps.meta.outputs.env }}" == "staging" ]; then
|
||||
echo "Injecting staging variables from wrangler.jsonc..."
|
||||
jq -r '.env.staging.vars | to_entries[] | "export \(.key)=\(.value|@sh)"' wrangler.jsonc | source /dev/stdin
|
||||
else
|
||||
echo "Injecting production variables from wrangler.jsonc..."
|
||||
jq -r '.vars | to_entries[] | "export \(.key)=\(.value|@sh)"' wrangler.jsonc | source /dev/stdin
|
||||
fi
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm build
|
||||
env:
|
||||
CF_PAGES_BRANCH: ${{ github.ref_name }}
|
||||
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
|
||||
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
|
||||
|
||||
- name: Deploy Cloudflare Worker
|
||||
id: wrangler
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
environment: ${{ steps.meta.outputs.env }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.cmd }}
|
||||
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"hosts": ["modrinth.com", "www.modrinth.com", "staging.modrinth.com"]}' \
|
||||
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
|
||||
30
.github/workflows/frontend-pages.yml
vendored
30
.github/workflows/frontend-pages.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Clear pages cache
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
wait:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Cloudflare Pages deployment
|
||||
uses: WalshyDev/cf-pages-await@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: '9ddae624c98677d68d93df6e524a6061'
|
||||
project: 'frontend'
|
||||
commitHash: ${{ steps.push-changes.outputs.commit-hash }}
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"hosts": ["modrinth.com", "www.modrinth.com"]}' \
|
||||
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
|
||||
@@ -1,8 +1,7 @@
|
||||
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
|
||||
// Import directly from utils to avoid loading .vue files at config time
|
||||
import { LOCALES } from '@modrinth/ui/src/composables/i18n.ts'
|
||||
import serverSidedVue from '@vitejs/plugin-vue'
|
||||
import { promises as fs } from 'fs'
|
||||
import fs from 'fs/promises'
|
||||
import { defineNuxtConfig } from 'nuxt/config'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
|
||||
@@ -96,6 +95,11 @@ export default defineNuxtConfig({
|
||||
},
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['cloudflare:workers'],
|
||||
},
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
async 'nitro:config'(nitroConfig) {
|
||||
@@ -183,7 +187,7 @@ export default defineNuxtConfig({
|
||||
process.env.CF_PAGES_BRANCH ||
|
||||
// @ts-ignore
|
||||
globalThis.CF_PAGES_BRANCH ||
|
||||
'master',
|
||||
'main',
|
||||
hash:
|
||||
process.env.VERCEL_GIT_COMMIT_SHA ||
|
||||
process.env.CF_PAGES_COMMIT_SHA ||
|
||||
@@ -246,6 +250,11 @@ export default defineNuxtConfig({
|
||||
rollupConfig: {
|
||||
// @ts-expect-error because of rolldown-vite - completely fine though
|
||||
plugins: [serverSidedVue()],
|
||||
external: ['cloudflare:workers'],
|
||||
},
|
||||
preset: 'cloudflare_module',
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
},
|
||||
devtools: {
|
||||
@@ -308,11 +317,8 @@ function getFeatureFlagOverrides() {
|
||||
|
||||
function getDomain() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.SITE_URL) {
|
||||
return process.env.SITE_URL
|
||||
}
|
||||
// @ts-ignore
|
||||
else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
|
||||
if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
|
||||
// @ts-ignore
|
||||
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL
|
||||
} else if (process.env.HEROKU_APP_NAME) {
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "pnpm build"
|
||||
"cf-deploy": "pnpm run build && wrangler deploy --env preview",
|
||||
"cf-dev": "pnpm run build && wrangler dev --env preview",
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
@@ -30,7 +32,8 @@
|
||||
"typescript": "^5.4.5",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-component-type-helpers": "^3.1.8",
|
||||
"vue-tsc": "^2.0.24"
|
||||
"vue-tsc": "^2.0.24",
|
||||
"wrangler": "^4.54.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
let cachedRateLimitKey = undefined
|
||||
let rateLimitKeyPromise = undefined
|
||||
|
||||
async function getRateLimitKey(config) {
|
||||
if (config.rateLimitKey) return config.rateLimitKey
|
||||
if (cachedRateLimitKey !== undefined) return cachedRateLimitKey
|
||||
|
||||
if (!rateLimitKeyPromise) {
|
||||
rateLimitKeyPromise = (async () => {
|
||||
try {
|
||||
const { env } = await import('cloudflare:workers')
|
||||
return await env.RATE_LIMIT_IGNORE_KEY?.get()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
cachedRateLimitKey = await rateLimitKeyPromise
|
||||
return cachedRateLimitKey
|
||||
}
|
||||
|
||||
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
|
||||
const config = useRuntimeConfig()
|
||||
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
|
||||
@@ -7,7 +29,7 @@ export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
|
||||
}
|
||||
|
||||
if (import.meta.server) {
|
||||
options.headers['x-ratelimit-key'] = config.rateLimitKey
|
||||
options.headers['x-ratelimit-key'] = await getRateLimitKey(config)
|
||||
}
|
||||
|
||||
if (!skipAuth) {
|
||||
|
||||
@@ -13,6 +13,17 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
|
||||
try {
|
||||
// @ts-expect-error only avail in workers env
|
||||
const { env } = await import('cloudflare:workers')
|
||||
return await env.RATE_LIMIT_IGNORE_KEY?.get()
|
||||
} catch {
|
||||
// Not running in Cloudflare Workers environment
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function createModrinthClient(
|
||||
auth: Ref<{ token: string | undefined }>,
|
||||
config: { apiBaseUrl: string; archonBaseUrl: string; rateLimitKey?: string },
|
||||
@@ -24,7 +35,7 @@ export function createModrinthClient(
|
||||
const clientConfig: NuxtClientConfig = {
|
||||
labrinthBaseUrl: config.apiBaseUrl,
|
||||
archonBaseUrl: config.archonBaseUrl,
|
||||
rateLimitKey: config.rateLimitKey,
|
||||
rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore,
|
||||
features: [
|
||||
// for modrinth hosting
|
||||
// is skipped for normal reqs
|
||||
|
||||
59
apps/frontend/wrangler.jsonc
Normal file
59
apps/frontend/wrangler.jsonc
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "frontend",
|
||||
"compatibility_date": "2025-12-10",
|
||||
"main": "./.output/server/index.mjs",
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": "./.output/public/"
|
||||
},
|
||||
"compatibility_flags": ["nodejs_compat", "no_nodejs_compat_v2"],
|
||||
"preview_urls": true,
|
||||
"workers_dev": true,
|
||||
"limits": {
|
||||
"cpu_ms": 2000
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 0.001
|
||||
},
|
||||
"keep_vars": false,
|
||||
"secrets_store_secrets": [
|
||||
{
|
||||
"binding": "RATE_LIMIT_IGNORE_KEY",
|
||||
"store_id": "c9024fef252d4a53adf513feca64417d",
|
||||
"secret_name": "labrinth-production-ratelimit-key"
|
||||
}
|
||||
],
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production",
|
||||
"BASE_URL": "https://api.modrinth.com/v2/",
|
||||
"BROWSER_BASE_URL": "https://api.modrinth.com/v2/",
|
||||
"PYRO_BASE_URL": "https://archon.modrinth.com/",
|
||||
"STRIPE_PUBLISHABLE_KEY": "pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5"
|
||||
},
|
||||
"env": {
|
||||
"staging": {
|
||||
"observability": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 0.1
|
||||
},
|
||||
"routes": ["staging.modrinth.com/*"],
|
||||
"vars": {
|
||||
"ENVIRONMENT": "staging",
|
||||
"BASE_URL": "https://staging-api.modrinth.com/v2/",
|
||||
"BROWSER_BASE_URL": "https://staging-api.modrinth.com/v2/",
|
||||
"PYRO_BASE_URL": "https://staging-archon.modrinth.com/",
|
||||
"STRIPE_PUBLISHABLE_KEY": "pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b"
|
||||
},
|
||||
"secrets_store_secrets": [
|
||||
{
|
||||
"binding": "RATE_LIMIT_IGNORE_KEY",
|
||||
"store_id": "c9024fef252d4a53adf513feca64417d",
|
||||
"secret_name": "labrinth-staging-ratelimit-key"
|
||||
}
|
||||
],
|
||||
"preview_urls": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,11 @@ export class NuxtCircuitBreakerStorage implements CircuitBreakerStorage {
|
||||
export interface NuxtClientConfig extends ClientConfig {
|
||||
// TODO: do we want to provide this for tauri+base as well? its not used on app
|
||||
/**
|
||||
* Rate limit key for server-side requests
|
||||
* This is injected as x-ratelimit-key header on server-side
|
||||
* Rate limit key for server-side requests.
|
||||
* This is injected as x-ratelimit-key header on server-side.
|
||||
* Can be a string (for env var) or async function (for CF Secrets Store).
|
||||
*/
|
||||
rateLimitKey?: string
|
||||
rateLimitKey?: string | (() => Promise<string | undefined>)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,8 @@ export interface NuxtClientConfig extends ClientConfig {
|
||||
*/
|
||||
export class NuxtModrinthClient extends XHRUploadClient {
|
||||
declare protected config: NuxtClientConfig
|
||||
private rateLimitKeyResolved: string | undefined
|
||||
private rateLimitKeyPromise: Promise<string | undefined> | undefined
|
||||
|
||||
constructor(config: NuxtClientConfig) {
|
||||
super(config)
|
||||
@@ -87,6 +90,40 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the rate limit key, handling both string and async function values.
|
||||
* Results are cached for subsequent calls.
|
||||
*/
|
||||
private async resolveRateLimitKey(): Promise<string | undefined> {
|
||||
if (this.rateLimitKeyResolved !== undefined) {
|
||||
return this.rateLimitKeyResolved
|
||||
}
|
||||
|
||||
const key = this.config.rateLimitKey
|
||||
if (typeof key === 'string') {
|
||||
this.rateLimitKeyResolved = key
|
||||
} else if (typeof key === 'function') {
|
||||
if (!this.rateLimitKeyPromise) {
|
||||
this.rateLimitKeyPromise = key()
|
||||
}
|
||||
this.rateLimitKeyResolved = await this.rateLimitKeyPromise
|
||||
}
|
||||
|
||||
return this.rateLimitKeyResolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Override request to resolve rate limit key before calling super.
|
||||
* This allows async fetching of the key from CF Secrets Store.
|
||||
*/
|
||||
async request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
// @ts-expect-error - import.meta is provided by Nuxt
|
||||
if (import.meta.server) {
|
||||
await this.resolveRateLimitKey()
|
||||
}
|
||||
return super.request(path, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file with progress tracking
|
||||
*
|
||||
@@ -132,9 +169,10 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
...super.buildDefaultHeaders(),
|
||||
}
|
||||
|
||||
// Use the resolved key (populated by resolveRateLimitKey in request())
|
||||
// @ts-expect-error - import.meta is provided by Nuxt
|
||||
if (import.meta.server && this.config.rateLimitKey) {
|
||||
headers['x-ratelimit-key'] = this.config.rateLimitKey
|
||||
if (import.meta.server && this.rateLimitKeyResolved) {
|
||||
headers['x-ratelimit-key'] = this.rateLimitKeyResolved
|
||||
}
|
||||
|
||||
return headers
|
||||
|
||||
487
pnpm-lock.yaml
generated
487
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user