diff --git a/.cargo/config.toml b/.cargo/config.toml index bdeac036f2..d4ed40327b 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/merge-queue-ci-skipper/action.yml b/.github/merge-queue-ci-skipper/action.yml index 3a2474a6dd..18783bb80e 100644 --- a/.github/merge-queue-ci-skipper/action.yml +++ b/.github/merge-queue-ci-skipper/action.yml @@ -34,13 +34,13 @@ runs: using: 'composite' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Extract PR Number and Commit ID id: extract-pr-info - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | const githubRef = process.env.GITHUB_REF; @@ -90,20 +90,26 @@ runs: - name: Print PR Branch if: env.CAN_SKIP_CHECKS != 'false' shell: bash + env: + PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }} run: | - echo "PR Branch: ${{ steps.get-pr-branch.outputs.prBranch }}" + echo "PR Branch: $PR_BRANCH" - name: Check if PR branch contains the Merge Queue target commit ID if: env.CAN_SKIP_CHECKS != 'false' shell: bash + env: + PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }} + COMMIT_ID: ${{ steps.extract-pr-info.outputs.commitId }} + TARGET_BRANCH: ${{ steps.extract-pr-info.outputs.targetBranchName }} run: | # Get the branch name from previous steps - branch_name="origin/${{ steps.get-pr-branch.outputs.prBranch }}" - commit_id="${{ steps.extract-pr-info.outputs.commitId }}" + branch_name="origin/$PR_BRANCH" + commit_id="$COMMIT_ID" # Check if the branch history contains the commit - if git branch -r --contains "$commit_id" | grep -q "$branch_name"; then - echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with ${{ steps.extract-pr-info.outputs.targetBranchName }}." + if git branch -r --contains "$commit_id" | grep -qF "$branch_name"; then + echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with $TARGET_BRANCH." else echo "Branch '$branch_name' does not contain commit '$commit_id'. It is outdated. Setting CAN_SKIP_CHECKS to false." echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV" @@ -112,8 +118,10 @@ runs: - name: Compare PR Branch with Current Branch if: env.CAN_SKIP_CHECKS != 'false' shell: bash + env: + PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }} run: | - if git diff --quiet "origin/${{ steps.get-pr-branch.outputs.prBranch }}"; then + if git diff --quiet "origin/$PR_BRANCH"; then echo "No differences found. PR branch is identical with this merge queue branch." else echo "Differences detected. PR branch has been updated after PR was added to merge queue. Setting CAN_SKIP_CHECKS to false." @@ -122,9 +130,11 @@ runs: - name: Compute/publish skip result id: passed-checks - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: SECRET: ${{ inputs.secret }} + PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }} + TARGET_BRANCH: ${{ steps.extract-pr-info.outputs.targetBranchName }} with: github-token: ${{ inputs.secret != '' && inputs.secret || github.token }} script: | @@ -144,7 +154,7 @@ runs: const { data: branchProtection } = await github.rest.repos.getBranchProtection({ owner: context.repo.owner, repo: context.repo.repo, - branch: "${{ steps.extract-pr-info.outputs.targetBranchName }}", + branch: process.env.TARGET_BRANCH, }); const requiredCheckNames = branchProtection.required_status_checks.contexts; console.log(`requiredCheckNames = ${requiredCheckNames}`); @@ -152,7 +162,7 @@ runs: const { data: checks } = await github.rest.checks.listForRef({ owner: context.repo.owner, repo: context.repo.repo, - ref: "refs/heads/${{ steps.get-pr-branch.outputs.prBranch }}", + ref: `refs/heads/${process.env.PR_BRANCH}`, }); console.log(`checks.check_runs = ${checks.check_runs.map(check => `${check.status},${check.conclusion},${check.name};`)}`); diff --git a/.github/workflows/api-client-release.yml b/.github/workflows/api-client-release.yml index 9f777e913c..222aab6f05 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 3f3eec12d0..135cd1c403 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 79937c7a3b..c59d96bdac 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 5b614cf67d..1953ccedf4 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 3a516c20b4..8fa131f83f 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/cmd-deploy.yml b/.github/workflows/cmd-deploy.yml new file mode 100644 index 0000000000..182f2bd5aa --- /dev/null +++ b/.github/workflows/cmd-deploy.yml @@ -0,0 +1,24 @@ +name: Deploy Command +run-name: Deploy PR #${{ github.event.client_payload.github.payload.issue.number }} + +on: + repository_dispatch: + types: [deploy-command] + +permissions: + contents: read + actions: read + +jobs: + deploy: + if: ${{ github.event.client_payload.pull_request.head.repo.full_name == github.repository }} + uses: SparkUniverse/workflows/.github/workflows/deploy-command.yaml@main + secrets: + ARGOCD_DEPLOY_KEY: ${{ secrets.ARGOCD_DEPLOY_KEY }} + CMD_DISPATCH_GH_TOKEN: ${{ secrets.SLASH_CMD_GH_TOKEN }} + with: + application-set: labrinth + build-workflow: labrinth-build.yml + branch: ${{ github.event.client_payload.pull_request.head.ref }} + issue-number: ${{ github.event.client_payload.github.payload.issue.number }} + head-sha: ${{ github.event.client_payload.pull_request.head.sha }} diff --git a/.github/workflows/daedalus-docker.yml b/.github/workflows/daedalus-docker.yml index ed20f15ca9..a3999fd71c 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 5060693e67..7a1bf3d65d 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 4912cd6121..3dd45ee207 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 0c9d5f2d20..e1ca8b6818 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,22 +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 - env: - NODE_OPTIONS: --max-old-space-size=6144 + - 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 @@ -101,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' @@ -133,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 62c278321f..df5ceffa58 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 6146c37526..89de7713af 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-build.yml similarity index 51% rename from .github/workflows/labrinth-docker.yml rename to .github/workflows/labrinth-build.yml index 58d5aa7081..5fdc1ce080 100644 --- a/.github/workflows/labrinth-docker.yml +++ b/.github/workflows/labrinth-build.yml @@ -1,18 +1,19 @@ -name: docker-build +name: Labrinth Build on: push: branches: - 'main' + - 'prod' paths: - - .github/workflows/labrinth-docker.yml + - .github/workflows/labrinth-build.yml - 'apps/labrinth/**' - Cargo.toml - Cargo.lock pull_request: types: [opened, synchronize] paths: - - .github/workflows/labrinth-docker.yml + - .github/workflows/labrinth-build.yml - 'apps/labrinth/**' - Cargo.toml - Cargo.lock @@ -33,7 +34,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 @@ -69,23 +70,23 @@ jobs: echo "skip=false" >> $GITHUB_OUTPUT fi - docker: - runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + build: + name: Build Labrinth + 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 +94,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 @@ -130,42 +122,30 @@ jobs: cp -r apps/labrinth/migrations apps/labrinth/docker-stage/migrations cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Generate Docker image metadata - id: docker-meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.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 - # image manifest only. See: - # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images - DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + - name: Upload Docker context + if: needs.skip-if-clean.outputs.internal == 'true' + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3 with: - images: ghcr.io/modrinth/labrinth - labels: | - org.opencontainers.image.title=labrinth - org.opencontainers.image.description=Modrinth API - org.opencontainers.image.licenses=AGPL-3.0-only - annotations: | - org.opencontainers.image.title=labrinth - org.opencontainers.image.description=Modrinth API - org.opencontainers.image.licenses=AGPL-3.0-only + name: labrinth-docker-context + retention-days: 1 + path: apps/labrinth/docker-stage - - name: Login to GitHub Packages - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + docker-build: + needs: [skip-if-clean, build] + if: ${{ needs.skip-if-clean.outputs.internal == 'true' }} + uses: SparkUniverse/workflows/.github/workflows/docker-build.yaml@main + with: + image-name: labrinth + dockerfile-path: apps/labrinth/Dockerfile + artifacts-name: labrinth-docker-context - - name: Build and push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: ./apps/labrinth/docker-stage - file: ./apps/labrinth/Dockerfile - push: true - tags: ${{ steps.docker-meta.outputs.tags }} - labels: ${{ steps.docker-meta.outputs.labels }} - annotations: ${{ steps.docker-meta.outputs.annotations }} + deploy: + needs: [skip-if-clean, docker-build] + if: ${{ needs.skip-if-clean.outputs.internal == 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/prod') }} + uses: SparkUniverse/workflows/.github/workflows/argo-update.yaml@main + secrets: + ARGOCD_DEPLOY_KEY: ${{ secrets.ARGOCD_DEPLOY_KEY }} + with: + application-set: labrinth + branch: ${{ github.ref == 'refs/heads/prod' && 'main' || 'develop' }} + environment-name: ${{ github.ref == 'refs/heads/prod' && 'production' || 'staging' }} diff --git a/.github/workflows/prepare-pnpm-cache.yml b/.github/workflows/prepare-pnpm-cache.yml deleted file mode 100644 index 16c563c94f..0000000000 --- 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/slash-cmds.yml b/.github/workflows/slash-cmds.yml new file mode 100644 index 0000000000..e35babeddf --- /dev/null +++ b/.github/workflows/slash-cmds.yml @@ -0,0 +1,20 @@ +name: Slash Command Dispatch + +on: + issue_comment: + types: [created] + +permissions: {} + +jobs: + dispatch-command: + if: ${{ github.event.sender.type == 'User' && contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association) }} + runs-on: namespace-profile-tiny-arm64 + steps: + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5.0.2 + with: + token: ${{ secrets.SLASH_CMD_GH_TOKEN }} + issue-type: pull-request + commands: | + deploy diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml index 4c7ad48e46..6b83d5bfde 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 aeb6a8f31d..57c0106330 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 ad4147a313..768b7a64b5 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 5ac8a874a2..b3eefef689 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" @@ -2478,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" @@ -2801,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", @@ -2811,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", @@ -2822,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", ] @@ -3856,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" @@ -4167,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" @@ -4765,7 +4806,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -5106,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" @@ -5435,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", @@ -5461,7 +5491,6 @@ dependencies = [ "json-patch 4.1.0", "labrinth", "lettre", - "lz4_flex", "meilisearch-sdk", "modrinth-content-management", "modrinth-util", @@ -5490,6 +5519,7 @@ dependencies = [ "serde_with", "sha1 0.10.6", "sha2", + "smallvec", "spdx", "sqlx", "sqlx-tracing", @@ -5511,6 +5541,7 @@ dependencies = [ "webauthn-rs-proto", "webp", "woothee", + "xredis", "yaserde", "zip 6.0.0", "zxcvbn", @@ -5576,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", @@ -5614,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" @@ -6030,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]] @@ -6300,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" @@ -7900,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" @@ -7964,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", @@ -8001,7 +8053,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.5", "tracing", "windows-sys 0.60.2", ] @@ -8227,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" @@ -8286,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]] @@ -9758,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]] @@ -9838,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" @@ -10940,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", @@ -11202,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", @@ -11429,7 +11505,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.5", "sync_wrapper", "tokio", "tokio-stream", @@ -13359,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 00e193ed6e..1529ce8910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "packages/modrinth-util", "packages/neverbounce", "packages/path-util", + "packages/xredis", ] [workspace.package] @@ -58,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" ] } @@ -72,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" @@ -89,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", @@ -152,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" @@ -189,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" } @@ -239,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 adee7387c6..74efbceb9b 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 7910ca37a7..e2f78fe965 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! -

-
- - - - - - -
-
-
+