diff --git a/.cargo/config.toml b/.cargo/config.toml index bdeac036f..d4ed40327 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,5 @@ # Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler -[target.'cfg(windows)'] +[target."cfg(windows)"] rustflags = ["-C", "link-args=/STACK:16777220"] [target.x86_64-pc-windows-msvc] diff --git a/.github/workflows/api-client-release.yml b/.github/workflows/api-client-release.yml index 9f777e913..222aab6f0 100644 --- a/.github/workflows/api-client-release.yml +++ b/.github/workflows/api-client-release.yml @@ -27,7 +27,7 @@ jobs: BUMP_TYPE: minor steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -47,7 +47,7 @@ jobs: - name: Setup Node if: steps.changes.outputs.changed == 'true' - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc registry-url: https://registry.npmjs.org @@ -63,7 +63,7 @@ jobs: - name: Restore pnpm cache if: steps.changes.outputs.changed == 'true' - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.pnpm-store.outputs.store-path }} key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/workflows/cancel-pr-workflow-on-merge.yml b/.github/workflows/cancel-pr-workflow-on-merge.yml index 3f3eec12d..135cd1c40 100644 --- a/.github/workflows/cancel-pr-workflow-on-merge.yml +++ b/.github/workflows/cancel-pr-workflow-on-merge.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 + uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1 with: workflow_id: all access_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/changelog-comment.yml b/.github/workflows/changelog-comment.yml index 79937c7a3..c59d96bda 100644 --- a/.github/workflows/changelog-comment.yml +++ b/.github/workflows/changelog-comment.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Post or update changelog comment - uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.CROWDIN_GH_TOKEN }} script: | diff --git a/.github/workflows/check-generic.yml b/.github/workflows/check-generic.yml index 5b614cf67..1953ccedf 100644 --- a/.github/workflows/check-generic.yml +++ b/.github/workflows/check-generic.yml @@ -12,14 +12,14 @@ jobs: typos: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: crate-ci/typos@6ac2ebd1b93eade61faf7e12688ad87a073fea59 # v1.46.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 # see tombi: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9 with: tool: tombi diff --git a/.github/workflows/check-rust.yml b/.github/workflows/check-rust.yml index 3a516c20b..8fa131f83 100644 --- a/.github/workflows/check-rust.yml +++ b/.github/workflows/check-rust.yml @@ -6,14 +6,15 @@ on: env: CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 SQLX_OFFLINE: true jobs: shear: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear diff --git a/.github/workflows/daedalus-docker.yml b/.github/workflows/daedalus-docker.yml index ed20f15ca..a3999fd71 100644 --- a/.github/workflows/daedalus-docker.yml +++ b/.github/workflows/daedalus-docker.yml @@ -35,7 +35,7 @@ jobs: if: ${{ always() }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check if workflow runs on an internal branch id: check-internal @@ -72,19 +72,15 @@ jobs: fi docker: - runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} - env: - SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }} - SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }} - RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }} + runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-medium-amd64' || 'ubuntu-latest' }} needs: [skip-if-clean] if: ${{ needs.skip-if-clean.outputs.skip != 'true' }} steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: rustflags: '' cache: false @@ -92,26 +88,6 @@ jobs: - name: Setup mold uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 - - name: Cache Cargo registry and index - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ~/.cargo/bin - key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Mount sccache disk cache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0 - with: - key: ${{ github.repository }}-daedalus-sccache - path: /mnt/sccache - - - name: Setup sccache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - - name: Build daedalus_client run: cargo build --release --package daedalus_client @@ -121,11 +97,11 @@ jobs: cp target/release/daedalus_client apps/daedalus_client/docker-stage/daedalus_client - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Generate Docker image metadata id: docker-meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: @@ -140,14 +116,14 @@ jobs: org.opencontainers.image.licenses=MIT - name: Login to GitHub Packages - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: ./apps/daedalus_client/docker-stage file: ./apps/daedalus_client/Dockerfile diff --git a/.github/workflows/daedalus-run.yml b/.github/workflows/daedalus-run.yml index 5060693e6..7a1bf3d65 100644 --- a/.github/workflows/daedalus-run.yml +++ b/.github/workflows/daedalus-run.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml index 4912cd612..3dd45ee20 100644 --- a/.github/workflows/frontend-deploy.yml +++ b/.github/workflows/frontend-deploy.yml @@ -16,10 +16,10 @@ on: workflow_dispatch: workflow_call: inputs: - environment: - required: true - type: string - description: 'The environment to deploy to (staging-preview or production-preview)' + preview: + required: false + type: boolean + description: 'Whether this is a preview deployment' head_sha: required: false type: string @@ -28,44 +28,56 @@ on: required: false type: string description: 'The branch name to deploy (defaults to github.ref_name)' + branch_alias: + required: false + type: string + description: 'The persistent preview alias for the branch' concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment || 'push' }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.preview && 'preview' || 'push' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }} jobs: deploy: - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: namespace-profile-modrinth-frontend permissions: contents: read deployments: write pull-requests: write steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Configure environment id: meta env: - ENV_INPUT: ${{ inputs.environment }} + PREVIEW_INPUT: ${{ inputs.preview }} REF: ${{ github.ref }} DEPLOY_SHA: ${{ inputs.head_sha || github.sha }} + BRANCH_ALIAS: ${{ inputs.branch_alias }} run: | echo "cmd=deploy" >> $GITHUB_OUTPUT SHA_SHORT="${DEPLOY_SHA::8}" - if [ "$ENV_INPUT" = "staging-preview" ]; then - echo "env=staging" >> $GITHUB_OUTPUT - echo "url=https://git-$SHA_SHORT-frontend-staging.modrinth.workers.dev" >> $GITHUB_OUTPUT - echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT - - elif [ "$ENV_INPUT" = "production-preview" ]; then + if [ "$PREVIEW_INPUT" = "true" ]; then + URL="https://git-$SHA_SHORT-frontend.modrinth.workers.dev" + STAGING_URL="https://git-$SHA_SHORT-staging-frontend.modrinth.workers.dev" echo "env=production" >> $GITHUB_OUTPUT - echo "url=https://git-$SHA_SHORT-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT - echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT + echo "url=$URL" >> $GITHUB_OUTPUT + echo "staging_url=$STAGING_URL" >> $GITHUB_OUTPUT + echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true --var CF_PAGES_URL:$URL" >> $GITHUB_OUTPUT + + STAGING_VARS=$(jq -r '.env.staging.vars | to_entries[] | "--var \(.key):\(.value)"' ./apps/frontend/wrangler.jsonc | tr '\n' ' ') + echo "staging_cmd=versions upload --preview-alias git-$SHA_SHORT-staging ${STAGING_VARS}--var PREVIEW:true --var CF_PAGES_URL:$STAGING_URL" >> $GITHUB_OUTPUT + + if [ -n "$BRANCH_ALIAS" ]; then + echo "branch_alias_url=https://$BRANCH_ALIAS-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT + echo "staging_branch_alias_url=https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT + echo "staging_branch_alias_cmd=versions upload --preview-alias $BRANCH_ALIAS-staging ${STAGING_VARS}--var PREVIEW:true --var CF_PAGES_URL:https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT + fi elif [ "$REF" = "refs/heads/main" ]; then echo "env=staging" >> $GITHUB_OUTPUT @@ -78,24 +90,23 @@ jobs: fi - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc - name: Enable Corepack run: corepack enable - - name: Get pnpm store path - id: pnpm-store - run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Restore pnpm cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + - name: Set up caches + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 with: - path: ${{ steps.pnpm-store.outputs.store-path }} - key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - pnpm-cache- + cache: | + rust + pnpm + apt + + - name: Set up Turborepo caching + uses: namespace-actions/setup-turbocache@f96bd6befa13ae74b2858a5be49573640d4636f5 # v0.0.2 - name: Inject build variables working-directory: ./apps/frontend @@ -120,11 +131,11 @@ jobs: CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }} CF_PAGES_URL: ${{ steps.meta.outputs.url }} BUILD_ENV: ${{ steps.meta.outputs.env }} - PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }} + PREVIEW: ${{ inputs.preview && 'true' || 'false' }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Create Sentry release and upload sourcemaps - uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3.6.0 + uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3.7.0 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: modrinth @@ -134,17 +145,53 @@ jobs: sourcemaps: ./apps/frontend/.output/server url_prefix: '~/' - - name: Deploy Cloudflare Worker - id: wrangler - uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 + - name: Install Wrangler CLI + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} - environment: ${{ steps.meta.outputs.env != 'production' && steps.meta.outputs.env || '' }} workingDirectory: ./apps/frontend packageManager: pnpm - wranglerVersion: '4.54.0' - command: ${{ steps.meta.outputs.cmd }} + wranglerVersion: '4.112.0' + command: --version + + - parallel: + - name: Deploy Cloudflare Worker + id: wrangler + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + environment: ${{ steps.meta.outputs.env != 'production' && steps.meta.outputs.env || '' }} + workingDirectory: ./apps/frontend + command: ${{ steps.meta.outputs.cmd }} + + - name: Deploy staging Cloudflare Worker preview + if: ${{ inputs.preview }} + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./apps/frontend + command: ${{ steps.meta.outputs.staging_cmd }} + + - name: Deploy persistent Cloudflare Worker preview + if: ${{ inputs.preview && inputs.branch_alias != '' }} + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./apps/frontend + command: versions upload --preview-alias ${{ inputs.branch_alias }} --var PREVIEW:true --var CF_PAGES_URL:${{ steps.meta.outputs.branch_alias_url }} + + - name: Deploy persistent staging Cloudflare Worker preview + if: ${{ inputs.preview && inputs.branch_alias != '' }} + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./apps/frontend + command: ${{ steps.meta.outputs.staging_branch_alias_cmd }} - name: Purge cache if: github.ref == 'refs/heads/prod' @@ -155,14 +202,23 @@ jobs: --data '{"hosts": ["modrinth.com", "www.modrinth.com", "staging.modrinth.com"]}' \ https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache - - name: Write deployment URL to file - if: ${{ inputs.environment != '' }} + - name: Write deployment URLs to files + if: ${{ inputs.preview }} run: | - echo "${{ steps.meta.outputs.url }}" > deployment-url-${{ inputs.environment }}.txt + echo "${{ steps.meta.outputs.url }}" > deployment-url-production.txt + echo "${{ steps.meta.outputs.staging_url }}" > deployment-url-staging.txt - - name: Upload deployment URL - if: ${{ inputs.environment != '' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: deployment-url-${{ inputs.environment }} - path: deployment-url-${{ inputs.environment }}.txt + - parallel: + - name: Upload production deployment URL + if: ${{ inputs.preview }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: deployment-url-production + path: deployment-url-production.txt + + - name: Upload staging deployment URL + if: ${{ inputs.preview }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: deployment-url-staging + path: deployment-url-staging.txt diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml index 66b988399..e1ca8b681 100644 --- a/.github/workflows/frontend-preview.yml +++ b/.github/workflows/frontend-preview.yml @@ -12,24 +12,34 @@ on: - '.github/workflows/frontend-preview.yml' jobs: + metadata: + if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + outputs: + branch-alias: ${{ steps.meta.outputs.branch_alias }} + steps: + - name: Configure branch alias + id: meta + run: echo "branch_alias=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + deploy: if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository + needs: metadata uses: ./.github/workflows/frontend-deploy.yml secrets: inherit concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.environment }} + group: ${{ github.workflow }}-${{ github.ref }}-deploy cancel-in-progress: true - strategy: - matrix: - environment: [staging-preview, production-preview] with: - environment: ${{ matrix.environment }} + preview: true head_sha: ${{ github.event.pull_request.head.sha }} head_ref: ${{ github.event.pull_request.head.ref }} + branch_alias: ${{ needs.metadata.outputs.branch-alias }} deploy-storybook: if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-2vcpu-ubuntu-2404 + needs: metadata + runs-on: namespace-profile-modrinth-frontend concurrency: group: ${{ github.workflow }}-${{ github.ref }}-storybook cancel-in-progress: true @@ -38,27 +48,21 @@ jobs: deployments: write steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc - name: Enable Corepack run: corepack enable - - name: Get pnpm store path - id: pnpm-store - run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Restore pnpm cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + - name: Set up caches + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 with: - path: ${{ steps.pnpm-store.outputs.store-path }} - key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - pnpm-cache- + cache: | + pnpm - name: Install dependencies working-directory: ./packages/ui @@ -66,6 +70,8 @@ jobs: - name: Build Storybook working-directory: ./packages/ui + env: + NODE_OPTIONS: --max-old-space-size=6144 run: pnpm run build-storybook - name: Configure short SHA @@ -74,20 +80,43 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: echo "sha_short=${HEAD_SHA::8}" >> $GITHUB_OUTPUT - - name: Deploy Storybook preview - uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 + - name: Install Wrangler CLI + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} workingDirectory: ./packages/ui packageManager: pnpm - wranglerVersion: '4.54.0' - command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }} + wranglerVersion: '4.112.0' + command: --version + + - parallel: + - name: Deploy Storybook preview + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./packages/ui + command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }} + + - name: Deploy persistent Storybook preview + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + env: + NODE_OPTIONS: --max-old-space-size=6144 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./packages/ui + command: versions upload --preview-alias ${{ needs.metadata.outputs.branch-alias }} comment: - if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository + if: | + !cancelled() + && github.repository_owner == 'modrinth' + && github.event.pull_request.head.repo.full_name == github.repository + && needs.deploy.result == 'success' runs-on: ubuntu-latest - needs: [deploy, deploy-storybook] + needs: [metadata, deploy, deploy-storybook] steps: - name: Download deployment URLs uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -99,16 +128,29 @@ jobs: id: urls env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BRANCH_ALIAS: ${{ needs.metadata.outputs.branch-alias }} + STORYBOOK_RESULT: ${{ needs.deploy-storybook.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt) - PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt) + STAGING_SHA_URL=$(cat deployment-url-staging.txt) + PRODUCTION_SHA_URL=$(cat deployment-url-production.txt) + STAGING_BRANCH_URL="https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" + PRODUCTION_BRANCH_URL="https://$BRANCH_ALIAS-frontend.modrinth.workers.dev" - echo "Production preview URL: $PRODUCTION_PREVIEW_URL" - echo "Staging preview URL: $STAGING_PREVIEW_URL" + if [ "$STORYBOOK_RESULT" = "success" ]; then + STORYBOOK_SHA_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" + STORYBOOK_BRANCH_URL="https://$BRANCH_ALIAS-storybook.modrinth.workers.dev" + else + STORYBOOK_SHA_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))" + STORYBOOK_BRANCH_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))" + fi - echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT - echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT - echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT + echo "staging-sha-url=$STAGING_SHA_URL" >> $GITHUB_OUTPUT + echo "production-sha-url=$PRODUCTION_SHA_URL" >> $GITHUB_OUTPUT + echo "storybook-sha-url=$STORYBOOK_SHA_URL" >> $GITHUB_OUTPUT + echo "staging-branch-url=$STAGING_BRANCH_URL" >> $GITHUB_OUTPUT + echo "production-branch-url=$PRODUCTION_BRANCH_URL" >> $GITHUB_OUTPUT + echo "storybook-branch-url=$STORYBOOK_BRANCH_URL" >> $GITHUB_OUTPUT - name: Find comment if: github.event_name == 'pull_request' @@ -131,9 +173,19 @@ jobs: Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }}) + ### Persistent + | Environment | URL | |-------------|-----| - | staging | ${{ steps.urls.outputs.staging-preview-url }} | - | production | ${{ steps.urls.outputs.production-preview-url }} | - | storybook | ${{ steps.urls.outputs.storybook-preview-url }} | + | staging | ${{ steps.urls.outputs.staging-branch-url }} | + | production | ${{ steps.urls.outputs.production-branch-url }} | + | storybook | ${{ steps.urls.outputs.storybook-branch-url }} | + + ### Latest commit + + | Environment | URL | + |-------------|-----| + | staging | ${{ steps.urls.outputs.staging-sha-url }} | + | production | ${{ steps.urls.outputs.production-sha-url }} | + | storybook | ${{ steps.urls.outputs.storybook-sha-url }} | edit-mode: replace diff --git a/.github/workflows/i18n-pull.yml b/.github/workflows/i18n-pull.yml index 62c278321..df5ceffa5 100644 --- a/.github/workflows/i18n-pull.yml +++ b/.github/workflows/i18n-pull.yml @@ -51,13 +51,13 @@ jobs: CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }} - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.ref }} token: ${{ secrets.CROWDIN_GH_TOKEN }} - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc @@ -90,7 +90,7 @@ jobs: echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT" - name: Download translations from Crowdin - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 with: upload_sources: false upload_translations: false diff --git a/.github/workflows/i18n-push.yml b/.github/workflows/i18n-push.yml index 6146c3752..89de7713a 100644 --- a/.github/workflows/i18n-push.yml +++ b/.github/workflows/i18n-push.yml @@ -56,13 +56,13 @@ jobs: CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }} - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.ref }} fetch-depth: 2 - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc @@ -83,7 +83,7 @@ jobs: echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT" - name: Upload translations to Crowdin - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 with: upload_sources: true upload_translations: false diff --git a/.github/workflows/labrinth-docker.yml b/.github/workflows/labrinth-docker.yml index 58d5aa708..ce8d8effa 100644 --- a/.github/workflows/labrinth-docker.yml +++ b/.github/workflows/labrinth-docker.yml @@ -33,7 +33,7 @@ jobs: if: ${{ always() }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check if workflow runs on an internal branch id: check-internal @@ -70,22 +70,21 @@ jobs: fi docker: - runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-modrinth-labrinth' || 'ubuntu-latest' }} needs: [skip-if-clean] if: ${{ needs.skip-if-clean.outputs.skip != 'true' }} env: SQLX_OFFLINE: 'true' GIT_HASH: ${{ github.sha }} - SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }} - SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }} + SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '20G' || '' }} RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }} RUST_MIN_STACK: '16777216' steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: rustflags: '' cache: false @@ -93,32 +92,23 @@ jobs: - name: Setup mold uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 + - name: Setup sccache + uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 + + - name: Set up caches + if: needs.skip-if-clean.outputs.internal == 'true' + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 + with: + cache: | + rust + apt + + - name: Configure sccache + if: needs.skip-if-clean.outputs.internal == 'true' + run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV" + - name: Install build dependencies run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake libcurl4-openssl-dev - #uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0 - #with: - # packages: cmake libcurl4-openssl-dev - # version: v2 # cache key - - - name: Cache Cargo registry and index - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ~/.cargo/bin - key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Mount sccache disk cache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0 - with: - key: ${{ github.repository }}-labrinth-sccache - path: /mnt/sccache - - - name: Setup sccache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - name: Build labrinth run: cargo build --profile release-labrinth --package labrinth @@ -131,11 +121,11 @@ jobs: cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Generate Docker image metadata id: docker-meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 env: # GitHub Packages requires annotations metadata in at least the index descriptor to show them # up properly in its UI it seems, but it's not clear about it, because the docs refer to the @@ -154,14 +144,14 @@ jobs: org.opencontainers.image.licenses=AGPL-3.0-only - name: Login to GitHub Packages - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: ./apps/labrinth/docker-stage file: ./apps/labrinth/Dockerfile diff --git a/.github/workflows/prepare-pnpm-cache.yml b/.github/workflows/prepare-pnpm-cache.yml deleted file mode 100644 index 16c563c94..000000000 --- a/.github/workflows/prepare-pnpm-cache.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Prepare pnpm cache - -on: - push: - paths: - - .github/workflows/prepare-pnpm-cache.yml - - package.json - - pnpm-lock.yaml - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }} - -jobs: - prepare: - if: github.repository_owner == 'modrinth' - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: .nvmrc - - - name: Enable Corepack - run: corepack enable - - - name: Get pnpm store path - id: pnpm-store - run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Cache pnpm - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: ${{ steps.pnpm-store.outputs.store-path }} - key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: Install dependencies - run: pnpm recursive install --frozen-lockfile diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 4c7ad48e4..6b83d5bfd 100644 --- a/.github/workflows/theseus-build.yml +++ b/.github/workflows/theseus-build.yml @@ -44,61 +44,62 @@ jobs: name: Build env: VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5 - # SCCACHE_DIR: '/mnt/sccache' - # SCCACHE_CACHE_SIZE: '10G' - # SCCACHE_MULTILEVEL_CHAIN: 'disk,s3' - SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/' - SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }} - SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }} - SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }} - AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }} RUSTC_WRAPPER: 'sccache' strategy: fail-fast: false matrix: - platform: [ - blacksmith-6vcpu-macos-26, - blacksmith-8vcpu-windows-2025, # At time of writing, Windows 4 vCPU VMs don't seem to actually exist - blacksmith-4vcpu-ubuntu-2404, + platform: + [ + namespace-profile-medium-amd64-macos, + namespace-profile-medium-amd64-windows, + namespace-profile-medium-amd64, ] include: - - platform: blacksmith-6vcpu-macos-26 + - platform: namespace-profile-medium-amd64-macos artifact-target-name: universal-apple-darwin - - platform: blacksmith-8vcpu-windows-2025 + - platform: namespace-profile-medium-amd64-windows artifact-target-name: x86_64-pc-windows-msvc - - platform: blacksmith-4vcpu-ubuntu-2404 + - platform: namespace-profile-medium-amd64 artifact-target-name: x86_64-unknown-linux-gnu runs-on: ${{ matrix.platform }} steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: rustflags: '' - target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }} + target: ${{ contains(matrix.artifact-target-name, 'darwin') && 'x86_64-apple-darwin' || '' }} - name: Setup mold - if: contains(matrix.platform, 'ubuntu') + if: contains(matrix.artifact-target-name, 'linux') uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 - name: Setup sccache uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + - name: Enable Corepack run: corepack enable - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - name: Set up caches + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 with: - node-version-file: .nvmrc - cache: 'pnpm' + cache: | + rust + pnpm + + - name: Configure sccache + run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV" - name: Generate tauri-dev.conf.json shell: bash @@ -116,11 +117,16 @@ jobs: EOF - name: Install Linux build dependencies - if: contains(matrix.platform, 'ubuntu') - uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0 + if: contains(matrix.artifact-target-name, 'linux') + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev + + - name: Setup Java + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 with: - packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev - version: v1 # cache key + distribution: temurin + java-version: 17 - name: Setup Dasel uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0 @@ -128,7 +134,7 @@ jobs: repo: TomWright/dasel tag: v2.8.1 extension-matching: disable - rename-to: ${{ contains(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }} + rename-to: ${{ contains(matrix.artifact-target-name, 'windows') && 'dasel.exe' || 'dasel' }} chmod: 0755 - name: Set application version and environment @@ -147,14 +153,11 @@ jobs: cp "packages/app-lib/.env.${BUILD_ENVIRONMENT}" packages/app-lib/.env - - name: Setup Turbo cache - uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14 - - name: Install dependencies run: pnpm install - name: Set up Windows code signing - if: contains(matrix.platform, 'windows') + if: contains(matrix.artifact-target-name, 'windows') shell: bash run: | if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then @@ -165,7 +168,7 @@ jobs: - name: Build macOS app run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }} - if: contains(matrix.platform, 'macos') + if: contains(matrix.artifact-target-name, 'darwin') env: TAURI_BUNDLER_DMG_IGNORE_CI: 'true' ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} @@ -180,7 +183,7 @@ jobs: - name: Build Linux app run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }} - if: contains(matrix.platform, 'ubuntu') + if: contains(matrix.artifact-target-name, 'linux') env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} @@ -192,7 +195,7 @@ jobs: $env:JAVA_HOME = "$env:JAVA_HOME_17_X64" ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }} Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue - if: contains(matrix.platform, 'windows') + if: contains(matrix.artifact-target-name, 'windows') env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} diff --git a/.github/workflows/theseus-release.yml b/.github/workflows/theseus-release.yml index aeb6a8f31..57c010633 100644 --- a/.github/workflows/theseus-release.yml +++ b/.github/workflows/theseus-release.yml @@ -15,7 +15,7 @@ jobs: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && startsWith(github.event.workflow_run.head_branch, 'v') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: namespace-profile-medium-amd64 env: VERSION_TAG: ${{ github.event.workflow_run.head_branch }} LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu) @@ -25,7 +25,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Verify ref is a tag env: diff --git a/.github/workflows/turbo-ci.yml b/.github/workflows/turbo-ci.yml index ad4147a31..768b7a64b 100644 --- a/.github/workflows/turbo-ci.yml +++ b/.github/workflows/turbo-ci.yml @@ -22,7 +22,7 @@ jobs: if: ${{ always() }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check if workflow runs on an internal branch id: check-internal @@ -62,7 +62,7 @@ jobs: build: name: Lint and Test - runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-modrinth-turbo' || 'ubuntu-latest' }} needs: [skip-if-clean] if: ${{ needs.skip-if-clean.outputs.skip != 'true' }} @@ -76,47 +76,47 @@ jobs: # since we don't want warnings to become errors # while developing) RUSTFLAGS: -Dwarnings - # sccache config (only populated for internal branches; secrets aren't - # available to forked PRs, and blacksmith stickydisk requires a - # blacksmith runner) - SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }} - SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }} - RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }} + # Redis config for tests + REDIS_TOPOLOGY: cluster + REDIS_CONNECTION_TYPE: multiplexed + REDIS_URL: redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005 + # Avoid stack overflows in tests + RUST_MIN_STACK: 134217728 steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 - - name: Install build dependencies - uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0 - with: - packages: cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev - version: v2 # cache key - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc - name: Enable Corepack run: corepack enable - - name: Get pnpm store path - id: pnpm-store - run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Restore pnpm cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + - name: Set up caches + if: needs.skip-if-clean.outputs.internal == 'true' + uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 with: - path: ${{ steps.pnpm-store.outputs.store-path }} - key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - pnpm-cache- + cache: | + rust + pnpm + apt + + - name: Set up Turborepo caching + if: needs.skip-if-clean.outputs.internal == 'true' + uses: namespace-actions/setup-turbocache@f96bd6befa13ae74b2858a5be49573640d4636f5 # v0.0.2 + + - name: Install apt dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: rustflags: '' components: clippy, rustfmt @@ -125,28 +125,8 @@ jobs: - name: Setup mold uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 - - name: Cache Cargo registry and index - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ~/.cargo/bin - key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Mount sccache disk cache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0 - with: - key: ${{ github.repository }}-turbo-sccache - path: /mnt/sccache - - - name: Setup sccache - if: needs.skip-if-clean.outputs.internal == 'true' - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - - name: Setup binstall - uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1 + uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 - name: Setup nextest run: cargo binstall --no-confirm --secure --locked cargo-nextest@0.9.133 @@ -154,16 +134,13 @@ jobs: # cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall # back to a cached cargo install - name: Setup cargo-sqlx - uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6 + uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7 with: tool: sqlx-cli@0.8.6 locked: false no-default-features: true features: rustls,postgres - - name: Setup Turbo cache - uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14 - - name: Install dependencies run: pnpm install @@ -185,7 +162,7 @@ jobs: - name: Start services if: steps.check-labrinth.outputs.needs_services == 'true' - run: docker compose up --wait + run: docker compose --profile clustered-redis up --wait - name: Setup labrinth environment and database if: steps.check-labrinth.outputs.needs_services == 'true' diff --git a/Cargo.lock b/Cargo.lock index ad23da412..b3eefef68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,7 +244,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.5", "time", "tracing", "url", @@ -458,6 +458,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -2133,6 +2139,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -2469,6 +2484,12 @@ dependencies = [ "spin 0.10.0", ] +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + [[package]] name = "crc32fast" version = "1.5.0" @@ -2792,7 +2813,7 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "deadpool" version = "0.12.3" -source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde" +source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b" dependencies = [ "deadpool-runtime", "num_cpus", @@ -2802,8 +2823,8 @@ dependencies = [ [[package]] name = "deadpool-redis" -version = "0.22.1" -source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde" +version = "0.23.0" +source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b" dependencies = [ "deadpool", "redis", @@ -2813,7 +2834,7 @@ dependencies = [ [[package]] name = "deadpool-runtime" version = "0.1.5" -source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde" +source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b" dependencies = [ "tokio", ] @@ -3259,6 +3280,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -3835,6 +3868,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.31" @@ -4146,6 +4185,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.3", + "hashbrown 0.16.0", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "group" version = "0.13.0" @@ -4744,7 +4806,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -5085,17 +5147,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "io-uring" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -5414,13 +5465,13 @@ dependencies = [ "bytes", "censor", "chrono", + "chumsky", "clap 4.5.48", "clickhouse", "color-eyre", "color-thief", "const_format", "dashmap", - "deadpool-redis", "derive_more 2.1.1", "dotenv-build", "dotenvy", @@ -5445,8 +5496,10 @@ dependencies = [ "modrinth-util", "muralpay", "murmur2", + "neverbounce", "paste", "path-util", + "postcard", "prometheus", "quick-xml 0.38.3", "rand 0.8.5", @@ -5466,6 +5519,7 @@ dependencies = [ "serde_with", "sha1 0.10.6", "sha2", + "smallvec", "spdx", "sqlx", "sqlx-tracing", @@ -5487,6 +5541,7 @@ dependencies = [ "webauthn-rs-proto", "webp", "woothee", + "xredis", "yaserde", "zip 6.0.0", "zxcvbn", @@ -5552,7 +5607,7 @@ dependencies = [ "quoted_printable", "rustls 0.23.32", "rustls-native-certs 0.8.1", - "socket2 0.6.1", + "socket2 0.6.5", "tokio", "tokio-rustls 0.26.4", "url", @@ -5590,9 +5645,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" @@ -6006,14 +6061,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6224,6 +6279,14 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neverbounce" +version = "0.1.0" +dependencies = [ + "reqwest 0.12.24", + "serde", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6268,6 +6331,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -7492,6 +7561,18 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -7856,6 +7937,21 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quartz_nbt" version = "0.2.9" @@ -7920,7 +8016,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.32", - "socket2 0.6.1", + "socket2 0.6.5", "thiserror 2.0.17", "tokio", "tracing", @@ -7957,7 +8053,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.5", "tracing", "windows-sys 0.60.2", ] @@ -8183,6 +8279,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -8242,26 +8347,32 @@ dependencies = [ [[package]] name = "redis" -version = "0.32.7" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" +checksum = "b0b9503711b03773e43b31668c7b5bd279ee7cd9b7d18cff7c23a42cc1d08e5a" dependencies = [ "ahash 0.8.12", + "arcstr", + "async-lock", "bytes", "cfg-if", "combine", + "crc16", "futures-util", "itoa", + "log", "num-bigint", "percent-encoding", "pin-project-lite", "r2d2", + "rand 0.10.1", "ryu", "sha1_smol", - "socket2 0.6.1", + "socket2 0.6.5", "tokio", "tokio-util", "url", + "xxhash-rust", ] [[package]] @@ -9714,12 +9825,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9794,6 +9905,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -10896,8 +11016,11 @@ dependencies = [ "fs4", "futures", "futures-lite 2.6.1", + "governor", "heck 0.5.0", "hickory-resolver 0.25.2", + "httpdate", + "image", "indicatif", "itertools 0.14.0", "modrinth-content-management", @@ -11158,30 +11281,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.1", + "socket2 0.6.5", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", @@ -11385,7 +11505,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.5", "sync_wrapper", "tokio", "tokio-stream", @@ -13315,6 +13435,31 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xredis" +version = "0.1.0" +dependencies = [ + "ariadne", + "chrono", + "dashmap", + "deadpool-redis", + "futures", + "lz4_flex", + "prometheus", + "redis", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985eec839aaf2a1270af8f4ebcf63cf9401cfd90f0902f97c28d9f104ffbde72" + [[package]] name = "yaserde" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index 301bed803..1529ce891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ members = [ "packages/modrinth-log", "packages/modrinth-maxmind", "packages/modrinth-util", + "packages/neverbounce", "packages/path-util", + "packages/xredis", ] [workspace.package] @@ -57,6 +59,7 @@ bytes = "1.10.1" censor = "0.3.0" chardetng = "0.1.17" chrono = "0.4.42" +chumsky = "0.9.3" cidre = { version = "0.15.0", default-features = false, features = [ "macos_15_0" ] } @@ -71,7 +74,7 @@ daedalus = { path = "packages/daedalus" } darling = { version = "0.23" } dashmap = "6.1.0" data-url = "0.3.2" -deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" } +deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "c7209b086829572f368f1194aba54bdea8dc540b", version = "0.23.0" } derive_more = "2.1.1" directories = "6.0.0" dirs = "6.0.0" @@ -88,10 +91,12 @@ fs4 = { version = "0.13.1", default-features = false } futures = "0.3.31" futures-lite = "2.6.1" futures-util = "0.3.31" +governor = "0.10.4" heck = "0.5.0" hex = "0.4.3" hickory-resolver = "0.25.2" hmac = "0.12.1" +httpdate = "1.0.3" hyper = "1.7.0" hyper-rustls = { version = "0.27.7", default-features = false, features = [ "aws-lc-rs", @@ -118,6 +123,12 @@ lettre = { version = "0.11.19", default-features = false, features = [ "tokio1", "tokio1-rustls", ] } +lz4_flex = { version = "0.11.5", default-features = false, features = [ + "checked-decode", + "safe-decode", + "safe-encode", + "std", +] } maxminddb = "0.26.0" meilisearch-sdk = { version = "0.30.0", default-features = false } modrinth-content-management = { path = "packages/modrinth-content-management" } @@ -126,6 +137,7 @@ modrinth-util = { path = "packages/modrinth-util" } muralpay = { path = "packages/muralpay" } murmur2 = "0.1.0" native-dialog = "0.9.2" +neverbounce = { path = "packages/neverbounce" } notify = { version = "8.2.0", default-features = false } notify-debouncer-mini = { version = "0.7.0", default-features = false } objc2-app-kit = { version = "0.3.2", default-features = false } @@ -135,6 +147,7 @@ paste = "1.0.15" path-util = { path = "packages/path-util" } phf = { version = "0.13.1", features = ["macros"] } png = "0.18.0" +postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } proc-macro2 = { version = "1.0" } prometheus = "0.14.0" quartz_nbt = "0.2.9" @@ -143,7 +156,7 @@ quote = { version = "1.0" } rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9 rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9 rdkafka = { version = "0.36.2", features = ["cmake-build"] } -redis = "0.32.7" +redis = "1.4.1" regex = "1.12.2" reqwest = { version = "0.12.24", default-features = false } rgb = "0.8.52" @@ -180,6 +193,7 @@ sha1 = "0.10.6" sha1_smol = { version = "1.0.1", features = ["std"] } sha2 = "0.10.9" shlex = "1.3.0" +smallvec = "1.15.1" spdx = "0.12.0" sqlx = { version = "0.8.6", default-features = false } sqlx-tracing = { path = "packages/sqlx-tracing" } @@ -230,6 +244,7 @@ windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62 windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62 winreg = "0.55.0" woothee = "0.13.0" +xredis = { path = "packages/xredis" } yaserde = "0.12.0" zbus = "5.11.0" zip = { version = "6.0.0", default-features = false, features = [ diff --git a/_typos.toml b/_typos.toml index adee7387c..74efbceb9 100644 --- a/_typos.toml +++ b/_typos.toml @@ -8,8 +8,11 @@ extend-exclude = [ "packages/blog/", # contains licenses like `CC-BY-ND-4.0` "packages/moderation/src/data/stages/license.ts", + "packages/moderation/src/utils.ts", # contains payment card IDs like `IY1VMST1MOXS` which are flagged "apps/labrinth/src/queue/payouts/mod.rs", + # contains domain names with deliberate typos + "apps/labrinth/assets/disposable_email_blocklist.txt" ] [default.extend-words] diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 7910ca37a..e2f78fe96 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -10,24 +10,21 @@ import { } from '@modrinth/api-client' import { ArrowBigUpDashIcon, - ChangeSkinIcon, + ChevronLeftIcon, + ChevronRightIcon, CompassIcon, - ExternalIcon, HomeIcon, - LeftArrowIcon, LibraryIcon, LogInIcon, LogOutIcon, NewspaperIcon, - NotepadTextIcon, PlusIcon, RefreshCwIcon, RightArrowIcon, ServerStackIcon, SettingsIcon, + ShirtIcon, UserIcon, - WorldIcon, - XIcon, } from '@modrinth/assets' import { Admonition, @@ -39,6 +36,7 @@ import { CreationFlowModal, defineMessages, I18nDebugPanel, + IntlFormatted, LoadingBar, NewsArticleCard, NotificationPanel, @@ -49,6 +47,7 @@ import { provideNotificationManager, providePageContext, providePopupNotificationManager, + TextLogo, useDebugLogger, useFormatBytes, useHostingIntercom, @@ -63,11 +62,9 @@ import { fetch as tauriFetch } from '@tauri-apps/plugin-http' import { openUrl } from '@tauri-apps/plugin-opener' import { type } from '@tauri-apps/plugin-os' import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state' -import { $fetch } from 'ofetch' import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue' import { RouterView, useRoute, useRouter } from 'vue-router' -import ModrinthAppLogo from '@/assets/modrinth_app.svg?component' import AccountsCard from '@/components/ui/AccountsCard.vue' import AppActionBar from '@/components/ui/AppActionBar.vue' import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' @@ -76,31 +73,42 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue' import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue' +import MinecraftRequiredModal from '@/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' -import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue' import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue' +import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue' import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue' import NavButton from '@/components/ui/NavButton.vue' import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' +import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue' import SplashScreen from '@/components/ui/SplashScreen.vue' +import SurveyPopup from '@/components/ui/SurveyPopup.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { config } from '@/config' -import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' +import { + ads_consent_listener, + hide_ads_window, + init_ads_window, + perform_ads_consent_action, + should_show_ads_consent_popup, + show_ads_window, +} from '@/helpers/ads.js' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { check_reachable } from '@/helpers/auth.js' import { get_user, get_version } from '@/helpers/cache.js' import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js' import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install' -import { list, run } from '@/helpers/instance' -import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts' +import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance' +import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts' import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' import { get_opening_command, initialize_state } from '@/helpers/state' import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts' +import { parse_modrinth_user_link } from '@/helpers/users' import { areUpdatesEnabled, enqueueUpdateForInstallation, @@ -122,6 +130,7 @@ import { openAppUpdateChangelog, setAppUpdateActions, } from '@/providers/app-update.ts' +import { createBreadcrumbManager, provideBreadcrumbManager } from '@/providers/breadcrumbs' import { createContentInstall, provideContentInstall } from '@/providers/content-install' import { provideAppUpdateDownloadProgress, @@ -133,20 +142,36 @@ import { setupAuthProvider } from '@/providers/setup/auth' import { setupLoadingStateProvider } from '@/providers/setup/loading-state' import { useError } from '@/store/error.js' import { useTheming } from '@/store/state' +import { appMessages } from '@/utils/app-messages' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' import { get_available_capes, get_available_skins } from './helpers/skins' import { AppNotificationManager } from './providers/app-notifications' import { AppPopupNotificationManager } from './providers/app-popup-notifications' +import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal' const themeStore = useTheming() const router = useRouter() const route = useRoute() +const breadcrumbManager = createBreadcrumbManager() +provideBreadcrumbManager(breadcrumbManager) +const canNavigateBack = ref(false) +const canNavigateForward = ref(false) + +function updateHistoryNavigationState() { + const historyState = window.history.state + canNavigateBack.value = historyState?.back != null + canNavigateForward.value = historyState?.forward != null +} + +updateHistoryNavigationState() + const APP_LEFT_NAV_WIDTH = '4rem' const APP_SIDEBAR_WIDTH = 300 const INTERCOM_BUBBLE_DEFAULT_PADDING = 20 const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime() const credentials = ref() +let credentialsRefreshId = 0 const sidebarToggled = ref(true) const unsubscribeSidebarToggle = themeStore.$subscribe(() => { sidebarToggled.value = !themeStore.toggleSidebar @@ -184,12 +209,15 @@ const { handleError, addNotification } = notificationManager const popupNotificationManager = new AppPopupNotificationManager() providePopupNotificationManager(popupNotificationManager) const { addPopupNotification } = popupNotificationManager +let adsConsentPopupId = null +let unlistenAdsConsent const appVersion = getVersion() const tauriApiClient = new TauriModrinthClient({ userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`, labrinthBaseUrl: config.labrinthBaseUrl, archonBaseUrl: config.archonBaseUrl, + sharedInstancesBaseUrl: config.sharedInstancesBaseUrl, features: [ new NodeAuthFeature({ getAuth: () => nodeAuthState.getAuth?.() ?? null, @@ -213,9 +241,30 @@ const { data: authenticatedModrinthUser } = useQuery({ enabled: () => !!credentials.value?.session, retry: false, }) +useQuery({ + queryKey: computed(() => ['shared-instance-eligibility', credentials.value?.user?.id]), + queryFn: can_current_user_use_shared_instances, + enabled: () => !!credentials.value?.session && !!credentials.value?.user?.id, + retry: false, + staleTime: Infinity, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, +}) +const hasPlus = computed( + () => + !!credentials.value?.user && + (hasMidasBadge(credentials.value.user) || + hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), +) +const showAd = computed( + () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, +) +const adConsentAvailable = computed(() => credentials.value !== undefined && !hasPlus.value) providePageContext({ hierarchicalSidebarAvailable: ref(true), - showAds: ref(false), + showAds: showAd, + adConsentAvailable, floatingActionBarOffsets: { left: ref(APP_LEFT_NAV_WIDTH), right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')), @@ -226,7 +275,7 @@ providePageContext({ themeStore.getFeatureFlag('server_ram_as_bytes_always_on'), ), }, - openExternalUrl: (url) => openUrl(url), + openExternalUrl: (url) => void openUrl(url), }) provideModalBehavior({ noblur: computed(() => !themeStore.advancedRendering), @@ -249,8 +298,10 @@ const { } = setupProviders(notificationManager, popupNotificationManager) const news = ref([]) -const availableSurvey = ref(false) const displayedServerInviteNotifications = new Set() +const serverInvitePopupNotificationIds = new Set() +let liveNotificationGeneration = 0 +let liveNotificationsEnabled = true const offline = ref(!navigator.onLine) window.addEventListener('offline', () => { @@ -295,6 +346,12 @@ const authUnreachable = computed(() => { onMounted(async () => { await useCheckDisableMouseover() + try { + unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired) + handleAdsConsentRequired(await should_show_ads_consent_popup()) + } catch (error) { + handleError(error) + } document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick) @@ -308,6 +365,7 @@ onUnmounted(async () => { unsubscribeSidebarToggle() clearDelayedUpdatePopup() + await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -332,8 +390,125 @@ const messages = defineMessages({ defaultMessage: 'Minecraft authentication servers may be down right now. Check your internet connection and try again later.', }, + adsConsentTitle: { + id: 'app.ads-consent.title', + defaultMessage: 'Your privacy and how ads support Modrinth', + }, + adsConsentBody: { + id: 'app.ads-consent.body', + defaultMessage: + 'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance.', + }, + adsConsentManage: { + id: 'app.ads-consent.manage', + defaultMessage: 'Manage preferences', + }, + adsConsentReject: { + id: 'app.ads-consent.reject', + defaultMessage: 'Reject all', + }, + adsConsentAccept: { + id: 'app.ads-consent.accept', + defaultMessage: 'Accept all', + }, + home: { + id: 'app.nav.home', + defaultMessage: 'Home', + }, + library: { + id: 'app.nav.library', + defaultMessage: 'Library', + }, + modrinthHosting: { + id: 'app.nav.modrinth-hosting', + defaultMessage: 'Modrinth Hosting', + }, + createNewInstance: { + id: 'app.nav.create-new-instance', + defaultMessage: 'Create new instance', + }, + modrinthAccount: { + id: 'app.nav.modrinth-account', + defaultMessage: 'Modrinth account', + }, + signedInAs: { + id: 'app.nav.signed-in-as', + defaultMessage: 'Signed in as {username}', + }, + signInToModrinthAccount: { + id: 'app.nav.sign-in-to-modrinth-account', + defaultMessage: 'Sign in to a Modrinth account', + }, + restarting: { + id: 'app.restarting', + defaultMessage: 'Restarting...', + }, + upgradeToModrinthPlus: { + id: 'app.nav.upgrade-to-modrinth-plus', + defaultMessage: 'Upgrade to Modrinth+', + }, + news: { + id: 'app.news.title', + defaultMessage: 'News', + }, + viewAllNews: { + id: 'app.news.view-all', + defaultMessage: 'View all news', + }, + playingAs: { + id: 'app.sidebar.playing-as', + defaultMessage: 'Playing as', + }, }) +function handleAdsConsentRequired(required) { + if (!required) { + if (adsConsentPopupId !== null) { + popupNotificationManager.removeNotification(adsConsentPopupId) + adsConsentPopupId = null + } + return + } + + if ( + adsConsentPopupId !== null && + popupNotificationManager.getNotifications().some((item) => item.id === adsConsentPopupId) + ) { + return + } + + const notification = addPopupNotification({ + title: formatMessage(messages.adsConsentTitle), + text: formatMessage(messages.adsConsentBody), + type: 'info', + hideIcon: true, + autoCloseMs: null, + dismissible: false, + buttons: [ + { + label: formatMessage(messages.adsConsentManage), + action: () => perform_ads_consent_action('manage').catch(handleError), + color: 'standard', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentReject), + action: () => perform_ads_consent_action('reject').catch(handleError), + color: 'brand', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentAccept), + action: () => perform_ads_consent_action('accept').catch(handleError), + color: 'brand', + keepOpen: true, + }, + ], + }) + + adsConsentPopupId = notification.id +} + async function setupApp() { const { native_decorations, @@ -403,7 +578,7 @@ async function setupApp() { addNotification({ title: 'Warning', text: e.message, - type: 'warn', + type: 'warning', }), ) @@ -452,12 +627,6 @@ async function setupApp() { settings.pending_update_toast_for_version = null await setSettings(settings) } - - if (osType === 'windows') { - await processPendingSurveys() - } else { - console.info('Skipping user surveys on non-Windows platforms') - } } const stateFailed = ref(false) @@ -501,6 +670,7 @@ router.beforeEach(() => { routerToken = loading.begin() }) router.afterEach((to, from, failure) => { + updateHistoryNavigationState() trackEvent('PageView', { path: to.path, fromPath: from.path, @@ -588,6 +758,7 @@ watch(stateInitialized, (ready) => { const error = useError() const errorModal = ref() const minecraftAuthErrorModal = ref() +const minecraftRequiredModal = ref() const contentInstall = createContentInstall({ router, handleError }) provideContentInstall(contentInstall) @@ -637,9 +808,12 @@ const contentInstallModpackAlreadyInstalledModal = ref() const addServerToInstanceModal = ref() const incompatibilityWarningModal = ref() const installToPlayModal = ref() +const sharedInstanceInviteHandler = ref() const updateToPlayModal = ref() -const modrinthLoginFlowWaitModal = ref() +const modrinthLoginModal = ref() +const appSettingsModal = ref() +provide(appSettingsModalOpenProfileKey, () => appSettingsModal.value?.showProfile()) watch(incompatibilityWarningModal, (modal) => { if (modal) { @@ -647,8 +821,12 @@ watch(incompatibilityWarningModal, (modal) => { } }) -setupAuthProvider(credentials, async (_redirectPath) => { - await signIn() +setupAuthProvider(credentials, async (_redirectPath, flow, options) => { + if (options?.showModal === false) { + await signIn(flow) + } else { + await requestSignIn(flow) + } }) async function validateSession(sessionToken) { @@ -665,23 +843,35 @@ async function validateSession(sessionToken) { } async function fetchCredentials() { + const hadSession = !!credentials.value?.session + const refreshId = ++credentialsRefreshId + credentials.value = undefined + const creds = await getCreds().catch(handleError) + if (refreshId !== credentialsRefreshId) return + if (!creds && hadSession) clearLiveNotifications() + if (creds && creds.user_id) { if (creds.session && !(await validateSession(creds.session))) { + if (refreshId !== credentialsRefreshId) return + + clearLiveNotifications() await logout().catch(handleError) + if (refreshId !== credentialsRefreshId) return + credentials.value = null return } creds.user = await get_user(creds.user_id, 'bypass').catch(handleError) + if (refreshId !== credentialsRefreshId) return } credentials.value = creds ?? null + liveNotificationsEnabled = !!creds?.session } -async function signIn() { - modrinthLoginFlowWaitModal.value.show() - +async function signIn(flow = 'sign-in') { try { - await login() + await login(flow) await fetchCredentials() } catch (error) { if ( @@ -693,27 +883,31 @@ async function signIn() { } else { handleError(error) } - } finally { - modrinthLoginFlowWaitModal.value.hide() } } +async function requestSignIn(flow = 'sign-in') { + await modrinthLoginModal.value?.showSigningIn(flow) +} + +async function requestModrinthAuth(flow = 'sign-in') { + await signIn(flow) + return !!credentials.value?.session +} + async function logOut() { + await performLogOut() +} + +async function performLogOut() { + credentialsRefreshId++ + credentials.value = undefined + clearLiveNotifications() + await logout().catch(handleError) await fetchCredentials() } -const hasPlus = computed( - () => - !!credentials.value?.user && - (hasMidasBadge(credentials.value.user) || - hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), -) - -const showAd = computed( - () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, -) - async function fetchIntercomToken() { const creds = await getCreds() if (!creds?.session) { @@ -740,19 +934,28 @@ async function fetchIntercomToken() { return await response.json() } -watch(showAd, () => { - if (!showAd.value) { - hide_ads_window(true) - } else { - init_ads_window(true) - } -}) +watch( + [showAd, adConsentAvailable], + async ([showAds, canManageConsent]) => { + if (showAds) { + await init_ads_window(true) + return + } + + await hide_ads_window(true) + if (canManageConsent) { + await init_ads_window() + } + }, + { immediate: true }, +) onMounted(() => { invoke('show_window') error.setErrorModal(errorModal.value) error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value) + error.setMinecraftRequiredModal(minecraftRequiredModal.value) setContentIncompatibilityWarningModal(incompatibilityWarningModal.value) setContentInstallModal(modInstallModal.value) @@ -817,34 +1020,52 @@ async function declineServerInviteNotification(notification) { function openServerInviteInviterProfile(inviterName) { if (!inviterName) return - openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`) + void router.push(`/user/${encodeURIComponent(inviterName)}`) } async function handleLiveNotification(notification) { - if (notification?.body?.type !== 'server_invite' || notification.read) return - if (displayedServerInviteNotifications.has(notification.id)) return + if (!liveNotificationsEnabled || !notification?.body || notification.read) return + if (await sharedInstanceInviteHandler.value?.handleNotification(notification)) return - displayedServerInviteNotifications.add(notification.id) + if (notification.body.type === 'server_invite') { + if (displayedServerInviteNotifications.has(notification.id)) return - const serverName = - typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server' - const inviterId = notification.body.invited_by - const invitedBy = - typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null + const generation = liveNotificationGeneration + displayedServerInviteNotifications.add(notification.id) - addPopupNotification({ - title: serverName, - autoCloseMs: null, - toast: { - type: 'server-invite', - actorName: invitedBy?.username ?? null, - actorAvatarUrl: invitedBy?.avatar_url ?? null, - entityName: serverName, - onAccept: () => acceptServerInviteNotification(notification), - onDecline: () => declineServerInviteNotification(notification), - onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null), - }, - }) + const serverName = + typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server' + const inviterId = notification.body.invited_by + const invitedBy = + typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null + if (generation !== liveNotificationGeneration) return + + const popupNotification = addPopupNotification({ + title: serverName, + autoCloseMs: null, + toast: { + type: 'server-invite', + actorName: invitedBy?.username ?? null, + actorAvatarUrl: invitedBy?.avatar_url ?? null, + entityName: serverName, + onAccept: () => acceptServerInviteNotification(notification), + onDecline: () => declineServerInviteNotification(notification), + onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null), + }, + }) + serverInvitePopupNotificationIds.add(popupNotification.id) + } +} + +function clearLiveNotifications() { + liveNotificationGeneration++ + liveNotificationsEnabled = false + for (const id of serverInvitePopupNotificationIds) { + popupNotificationManager.removeNotification(id) + } + displayedServerInviteNotifications.clear() + serverInvitePopupNotificationIds.clear() + sharedInstanceInviteHandler.value?.clearNotifications() } async function handleCommand(e) { @@ -871,6 +1092,9 @@ async function handleCommand(e) { }) } } else if (e.event === 'LaunchInstance') { + const instance = await getInstance(e.id).catch(handleError) + if (!instance || instance.quarantined) return + if (e.server) { await start_join_server(e.id, e.server).catch(handleError) } else if (e.singleplayer_world) { @@ -878,6 +1102,8 @@ async function handleCommand(e) { } else { await run(e.id).catch(handleError) } + } else if (e.event === 'InstallSharedInstanceInvite') { + await sharedInstanceInviteHandler.value?.installFromInviteId(e.invite_id) } else if (e.event === 'InstallServer') { await router.push(`/project/${e.id}`) await playServerProject(e.id).catch(handleError) @@ -1235,8 +1461,11 @@ function handleClick(e) { !target.href.startsWith('https://tauri.localhost') && !target.href.startsWith('http://tauri.localhost') ) { + const userPath = parse_modrinth_user_link(target.href) const parsed = parseModrinthLink(target.href) - if (target.target !== '_blank' && parsed) { + if (userPath) { + void router.push(userPath) + } else if (target.target !== '_blank' && parsed) { void openModrinthProjectLinkInApp(parsed) } else { openUrl(target.href) @@ -1263,115 +1492,6 @@ function handleAuxClick(e) { } } -function cleanupOldSurveyDisplayData() { - const threeWeeksAgo = new Date() - threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21) - - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i) - - if (key.startsWith('survey-') && key.endsWith('-display')) { - const dateValue = new Date(localStorage.getItem(key)) - if (dateValue < threeWeeksAgo) { - localStorage.removeItem(key) - } - } - } -} - -async function openSurvey() { - if (!availableSurvey.value) { - console.error('No survey to open') - return - } - - const creds = await getCreds().catch(handleError) - const userId = creds?.user_id - - const formId = availableSurvey.value.tally_id - - const popupOptions = { - layout: 'modal', - width: 700, - autoClose: 2000, - hideTitle: true, - hiddenFields: { - user_id: userId, - }, - onOpen: () => console.info('Opened user survey'), - onClose: () => { - console.info('Closed user survey') - show_ads_window() - }, - onSubmit: () => console.info('Active user survey submitted'), - } - - try { - hide_ads_window() - if (window.Tally?.openPopup) { - console.info(`Opening Tally popup for user survey (form ID: ${formId})`) - dismissSurvey() - window.Tally.openPopup(formId, popupOptions) - } else { - console.warn('Tally script not yet loaded') - show_ads_window() - } - } catch (e) { - console.error('Error opening Tally popup:', e) - show_ads_window() - } - - console.info(`Found user survey to show with tally_id: ${formId}`) - window.Tally.openPopup(formId, popupOptions) -} - -function dismissSurvey() { - localStorage.setItem(`survey-${availableSurvey.value.id}-display`, new Date()) - availableSurvey.value = undefined -} - -async function processPendingSurveys() { - function isWithinLastTwoWeeks(date) { - const twoWeeksAgo = new Date() - twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14) - return date >= twoWeeksAgo - } - - cleanupOldSurveyDisplayData() - - const creds = await getCreds().catch(handleError) - const userId = creds?.user_id - - const instances = (await list().catch(handleError)) ?? [] - const isActivePlayer = instances.some( - (instance) => - isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created), - ) - - let surveys = [] - try { - surveys = await $fetch('https://api.modrinth.com/v2/surveys') - } catch (e) { - console.error('Error fetching surveys:', e) - } - - const surveyToShow = surveys.find( - (survey) => - !!( - localStorage.getItem(`survey-${survey.id}-display`) === null && - survey.type === 'tally_app' && - ((survey.condition === 'active_player' && isActivePlayer) || - (survey.assigned_users?.includes(userId) && !survey.dismissed_users?.includes(userId))) - ), - ) - - if (surveyToShow) { - availableSurvey.value = surveyToShow - } else { - console.info('No user survey to show') - } -} - provideAppUpdateDownloadProgress(appUpdateDownload) @@ -1394,15 +1514,15 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="flex items-center gap-4 text-contrast font-semibold text-xl select-none cursor-default" > - Restarting... + {{ formatMessage(messages.restarting) }} - + - +
- + - - - - - + + -
@@ -1471,18 +1587,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
- Signed in as - - - {{ credentials?.user?.username }} - + + + - - + - +
-
- -
- - +
+ +
+ + + + + +
- +
- -
-

Hey there Modrinth user!

-

- Would you mind answering a few questions about your experience with Modrinth App? -

-

- This feedback will go directly to the Modrinth team and help guide future updates! -

-
- - - - - - -
-
-
+