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 c401860968..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,26 +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' || '' }} - SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }} - SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', 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: ${{ 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 @@ -99,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 @@ -128,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: @@ -147,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 66b9883997..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,20 +80,43 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: echo "sha_short=${HEAD_SHA::8}" >> $GITHUB_OUTPUT - - name: Deploy Storybook preview - uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 + - name: Install Wrangler CLI + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} workingDirectory: ./packages/ui packageManager: pnpm - wranglerVersion: '4.54.0' - command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }} + wranglerVersion: '4.112.0' + command: --version + + - parallel: + - name: Deploy Storybook preview + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./packages/ui + command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }} + + - name: Deploy persistent Storybook preview + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 + env: + NODE_OPTIONS: --max-old-space-size=6144 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + workingDirectory: ./packages/ui + command: versions upload --preview-alias ${{ needs.metadata.outputs.branch-alias }} comment: - if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository + if: | + !cancelled() + && github.repository_owner == 'modrinth' + && github.event.pull_request.head.repo.full_name == github.repository + && needs.deploy.result == 'success' runs-on: ubuntu-latest - needs: [deploy, deploy-storybook] + needs: [metadata, deploy, deploy-storybook] steps: - name: Download deployment URLs uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -99,16 +128,29 @@ jobs: id: urls env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BRANCH_ALIAS: ${{ needs.metadata.outputs.branch-alias }} + STORYBOOK_RESULT: ${{ needs.deploy-storybook.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt) - PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt) + STAGING_SHA_URL=$(cat deployment-url-staging.txt) + PRODUCTION_SHA_URL=$(cat deployment-url-production.txt) + STAGING_BRANCH_URL="https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" + PRODUCTION_BRANCH_URL="https://$BRANCH_ALIAS-frontend.modrinth.workers.dev" - echo "Production preview URL: $PRODUCTION_PREVIEW_URL" - echo "Staging preview URL: $STAGING_PREVIEW_URL" + if [ "$STORYBOOK_RESULT" = "success" ]; then + STORYBOOK_SHA_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" + STORYBOOK_BRANCH_URL="https://$BRANCH_ALIAS-storybook.modrinth.workers.dev" + else + STORYBOOK_SHA_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))" + STORYBOOK_BRANCH_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))" + fi - echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT - echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT - echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT + echo "staging-sha-url=$STAGING_SHA_URL" >> $GITHUB_OUTPUT + echo "production-sha-url=$PRODUCTION_SHA_URL" >> $GITHUB_OUTPUT + echo "storybook-sha-url=$STORYBOOK_SHA_URL" >> $GITHUB_OUTPUT + echo "staging-branch-url=$STAGING_BRANCH_URL" >> $GITHUB_OUTPUT + echo "production-branch-url=$PRODUCTION_BRANCH_URL" >> $GITHUB_OUTPUT + echo "storybook-branch-url=$STORYBOOK_BRANCH_URL" >> $GITHUB_OUTPUT - name: Find comment if: github.event_name == 'pull_request' @@ -131,9 +173,19 @@ jobs: Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }}) + ### Persistent + | Environment | URL | |-------------|-----| - | staging | ${{ steps.urls.outputs.staging-preview-url }} | - | production | ${{ steps.urls.outputs.production-preview-url }} | - | storybook | ${{ steps.urls.outputs.storybook-preview-url }} | + | staging | ${{ steps.urls.outputs.staging-branch-url }} | + | production | ${{ steps.urls.outputs.production-branch-url }} | + | storybook | ${{ steps.urls.outputs.storybook-branch-url }} | + + ### Latest commit + + | Environment | URL | + |-------------|-----| + | staging | ${{ steps.urls.outputs.staging-sha-url }} | + | production | ${{ steps.urls.outputs.production-sha-url }} | + | storybook | ${{ steps.urls.outputs.storybook-sha-url }} | edit-mode: replace diff --git a/.github/workflows/i18n-pull.yml b/.github/workflows/i18n-pull.yml index 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-build.yml b/.github/workflows/labrinth-build.yml new file mode 100644 index 0000000000..5fdc1ce080 --- /dev/null +++ b/.github/workflows/labrinth-build.yml @@ -0,0 +1,151 @@ +name: Labrinth Build + +on: + push: + branches: + - 'main' + - 'prod' + paths: + - .github/workflows/labrinth-build.yml + - 'apps/labrinth/**' + - Cargo.toml + - Cargo.lock + pull_request: + types: [opened, synchronize] + paths: + - .github/workflows/labrinth-build.yml + - 'apps/labrinth/**' + - Cargo.toml + - Cargo.lock + merge_group: + types: [checks_requested] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }} + +jobs: + skip-if-clean: + name: Skip if merge_queue produces no diff + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.check.outputs.skip }} + internal: ${{ steps.check-internal.outputs.internal }} + if: ${{ always() }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check if workflow runs on an internal branch + id: check-internal + env: + EVENT_NAME: ${{ github.event_name }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + REPO: ${{ github.repository }} + run: | + if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then + echo "internal=true" >> $GITHUB_OUTPUT + else + echo "internal=false" >> $GITHUB_OUTPUT + fi + + - name: Merge Queue CI Check Skipper + id: merge-queue-ci-skipper + uses: ./.github/merge-queue-ci-skipper + with: + secret: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Check merge_group synthetic commit + id: check + run: | + # PR mode: never skip + if [ "${{ github.event_name }}" != "merge_group" ]; then + echo "skip=false" >> $GITHUB_OUTPUT + exit 0 + fi + + if [ "${{ steps.merge-queue-ci-skipper.outputs.skip-check }}" = "true" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + 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_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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + rustflags: '' + cache: false + + - 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 + + - name: Build labrinth + run: cargo build --profile release-labrinth --package labrinth + + - name: Stage Docker context + run: | + mkdir -p apps/labrinth/docker-stage + cp target/release-labrinth/labrinth apps/labrinth/docker-stage/labrinth + cp -r apps/labrinth/migrations apps/labrinth/docker-stage/migrations + cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets + + - name: Upload Docker context + if: needs.skip-if-clean.outputs.internal == 'true' + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3 + with: + name: labrinth-docker-context + retention-days: 1 + path: apps/labrinth/docker-stage + + 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 + + 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/labrinth-docker.yml b/.github/workflows/labrinth-docker.yml deleted file mode 100644 index ca249994b9..0000000000 --- a/.github/workflows/labrinth-docker.yml +++ /dev/null @@ -1,176 +0,0 @@ -name: docker-build - -on: - push: - branches: - - 'main' - paths: - - .github/workflows/labrinth-docker.yml - - 'apps/labrinth/**' - - Cargo.toml - - Cargo.lock - pull_request: - types: [opened, synchronize] - paths: - - .github/workflows/labrinth-docker.yml - - 'apps/labrinth/**' - - Cargo.toml - - Cargo.lock - merge_group: - types: [checks_requested] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }} - -jobs: - skip-if-clean: - name: Skip if merge_queue produces no diff - runs-on: ubuntu-latest - outputs: - skip: ${{ steps.check.outputs.skip }} - internal: ${{ steps.check-internal.outputs.internal }} - if: ${{ always() }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check if workflow runs on an internal branch - id: check-internal - env: - EVENT_NAME: ${{ github.event_name }} - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - REPO: ${{ github.repository }} - run: | - if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then - echo "internal=true" >> $GITHUB_OUTPUT - else - echo "internal=false" >> $GITHUB_OUTPUT - fi - - - name: Merge Queue CI Check Skipper - id: merge-queue-ci-skipper - uses: ./.github/merge-queue-ci-skipper - with: - secret: ${{ secrets.GH_ACCESS_TOKEN }} - - - name: Check merge_group synthetic commit - id: check - run: | - # PR mode: never skip - if [ "${{ github.event_name }}" != "merge_group" ]; then - echo "skip=false" >> $GITHUB_OUTPUT - exit 0 - fi - - if [ "${{ steps.merge-queue-ci-skipper.outputs.skip-check }}" = "true" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - else - echo "skip=false" >> $GITHUB_OUTPUT - fi - - docker: - runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || '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_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }} - SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', 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: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }} - steps: - - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - with: - rustflags: '' - cache: false - - - name: Setup mold - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0 - - - name: Install build dependencies - 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 - - - name: Stage Docker context - run: | - mkdir -p apps/labrinth/docker-stage - cp target/release-labrinth/labrinth apps/labrinth/docker-stage/labrinth - 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 - 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: Login to GitHub Packages - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - 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 }} 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 c0cd950d99..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,54 +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' || '' }} - SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }} - SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', 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: ${{ 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 @@ -132,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 @@ -161,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 @@ -192,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 19f6627e78..b3eefef689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.11.2" +version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" dependencies = [ "actix-codec", "actix-rt", @@ -75,7 +75,7 @@ dependencies = [ "derive_more 2.1.1", "encoding_rs", "flate2", - "foldhash 0.1.5", + "foldhash 0.2.0", "futures-core", "h2 0.3.27", "http 0.2.12", @@ -87,8 +87,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.2", - "sha1", + "rand 0.10.1", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" dependencies = [ "bytestring", "cfg-if", @@ -209,9 +209,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.11.0" +version = "4.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" dependencies = [ "actix-codec", "actix-http", @@ -228,7 +228,7 @@ dependencies = [ "cookie 0.16.2", "derive_more 2.1.1", "encoding_rs", - "foldhash 0.1.5", + "foldhash 0.2.0", "futures-core", "futures-util", "impl-more", @@ -244,7 +244,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.10", + "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" @@ -1127,7 +1133,7 @@ dependencies = [ "http-body-util", "md-5", "pin-project-lite", - "sha1", + "sha1 0.10.6", "sha2", "tracing", ] @@ -1499,7 +1505,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1511,6 +1517,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -1619,6 +1634,31 @@ dependencies = [ "serde_with", ] +[[package]] +name = "bon" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + [[package]] name = "borsh" version = "1.5.7" @@ -2099,6 +2139,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -2224,6 +2273,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -2424,11 +2479,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ "crc", - "digest", + "digest 0.10.7", "rustversion", "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" @@ -2515,6 +2576,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.29.6" @@ -2743,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", @@ -2753,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", @@ -2764,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", ] @@ -2791,7 +2861,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -2904,12 +2974,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + [[package]] name = "directories" version = "6.0.0" @@ -3127,7 +3208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -3151,7 +3232,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -3199,6 +3280,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -3775,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" @@ -4086,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" @@ -4402,7 +4524,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -4532,6 +4654,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -4675,7 +4806,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -4893,9 +5024,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "impl-more" -version = "0.1.9" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" [[package]] name = "indenter" @@ -5016,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" @@ -5345,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", @@ -5376,8 +5496,10 @@ dependencies = [ "modrinth-util", "muralpay", "murmur2", + "neverbounce", "paste", "path-util", + "postcard", "prometheus", "quick-xml 0.38.3", "rand 0.8.5", @@ -5390,12 +5512,14 @@ dependencies = [ "rust_iso3166", "rustls 0.23.32", "rusty-money", + "scalar_api_reference", "sentry", "serde", "serde_json", "serde_with", - "sha1", + "sha1 0.10.6", "sha2", + "smallvec", "spdx", "sqlx", "sqlx-tracing", @@ -5411,14 +5535,13 @@ dependencies = [ "url", "urlencoding", "utoipa", - "utoipa-actix-web", - "utoipa-scalar", "uuid 1.23.3", "validator", "webauthn-rs", "webauthn-rs-proto", "webp", "woothee", + "xredis", "yaserde", "zip 6.0.0", "zxcvbn", @@ -5484,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", @@ -5522,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" @@ -5822,7 +5945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -5938,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]] @@ -6156,6 +6279,14 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neverbounce" +version = "0.1.0" +dependencies = [ + "reqwest 0.12.24", + "serde", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6200,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" @@ -7424,6 +7561,18 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -7788,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" @@ -7852,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", @@ -7889,7 +8053,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.5", "tracing", "windows-sys 0.60.2", ] @@ -8115,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" @@ -8174,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]] @@ -8494,8 +8673,8 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -8508,6 +8687,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.106", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -8827,6 +9040,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scalar_api_reference" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d67c85edd56c41f364603eff688a330f01dee99bdfc84c005d3bc3d0721d6b2" +dependencies = [ + "actix-web", + "rust-embed", + "serde", + "serde_json", +] + [[package]] name = "scc" version = "2.4.0" @@ -9252,15 +9477,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -9455,7 +9680,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -9472,7 +9708,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -9505,7 +9741,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -9589,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]] @@ -9669,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" @@ -9782,7 +10027,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -9803,7 +10048,7 @@ dependencies = [ "rsa", "rust_decimal", "serde", - "sha1", + "sha1 0.10.6", "sha2", "smallvec", "sqlx-core", @@ -10749,6 +10994,7 @@ dependencies = [ "async-walkdir", "async_zip", "base64 0.22.1", + "bon", "bytemuck", "bytes", "chardetng", @@ -10770,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", @@ -11032,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", @@ -11259,7 +11505,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.5", "sync_wrapper", "tokio", "tokio-stream", @@ -11290,7 +11536,7 @@ dependencies = [ "constant_time_eq", "hmac", "rand 0.9.2", - "sha1", + "sha1 0.10.6", "sha2", ] @@ -11505,7 +11751,7 @@ dependencies = [ "rand 0.9.2", "rustls 0.23.32", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.17", "utf-8", ] @@ -11524,9 +11770,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -11757,17 +12003,6 @@ dependencies = [ "utoipa-gen", ] -[[package]] -name = "utoipa-actix-web" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f" -dependencies = [ - "actix-service", - "actix-web", - "utoipa", -] - [[package]] name = "utoipa-gen" version = "5.4.0" @@ -11782,18 +12017,6 @@ dependencies = [ "uuid 1.23.3", ] -[[package]] -name = "utoipa-scalar" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719" -dependencies = [ - "actix-web", - "serde", - "serde_json", - "utoipa", -] - [[package]] name = "uuid" version = "0.8.2" @@ -13212,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" @@ -13450,6 +13698,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index ac9044a826..1529ce8910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ members = [ "packages/modrinth-log", "packages/modrinth-maxmind", "packages/modrinth-util", + "packages/neverbounce", "packages/path-util", + "packages/xredis", ] [workspace.package] @@ -51,11 +53,13 @@ aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [ ] } base64 = "0.22.1" bitflags = "2.9.4" +bon = "3.9.3" bytemuck = "1.24.0" 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" ] } @@ -70,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" @@ -87,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", @@ -117,6 +123,12 @@ lettre = { version = "0.11.19", default-features = false, features = [ "tokio1", "tokio1-rustls", ] } +lz4_flex = { version = "0.11.5", default-features = false, features = [ + "checked-decode", + "safe-decode", + "safe-encode", + "std", +] } maxminddb = "0.26.0" meilisearch-sdk = { version = "0.30.0", default-features = false } modrinth-content-management = { path = "packages/modrinth-content-management" } @@ -125,6 +137,7 @@ modrinth-util = { path = "packages/modrinth-util" } muralpay = { path = "packages/muralpay" } murmur2 = "0.1.0" native-dialog = "0.9.2" +neverbounce = { path = "packages/neverbounce" } notify = { version = "8.2.0", default-features = false } notify-debouncer-mini = { version = "0.7.0", default-features = false } objc2-app-kit = { version = "0.3.2", default-features = false } @@ -134,6 +147,7 @@ paste = "1.0.15" path-util = { path = "packages/path-util" } phf = { version = "0.13.1", features = ["macros"] } png = "0.18.0" +postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } proc-macro2 = { version = "1.0" } prometheus = "0.14.0" quartz_nbt = "0.2.9" @@ -142,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" @@ -158,6 +172,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [ ] } rustls = "0.23.32" rusty-money = "0.4.1" +scalar_api_reference = { version = "0.2.2", default-features = false } secrecy = "0.10.3" sentry = { version = "0.45.0", default-features = false, features = [ "backtrace", @@ -178,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" } @@ -217,8 +233,6 @@ typed-path = "0.12.0" url = "2.5.7" urlencoding = "2.1.3" utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] } -utoipa-actix-web = { version = "0.1.2" } -utoipa-scalar = { version = "0.3.0", default-features = false } uuid = "1.18.1" validator = "0.20.0" webauthn-rs = "0.5.5" @@ -230,6 +244,7 @@ windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62 windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62 winreg = "0.55.0" woothee = "0.13.0" +xredis = { path = "packages/xredis" } yaserde = "0.12.0" zbus = "5.11.0" zip = { version = "6.0.0", default-features = false, features = [ diff --git a/README.md b/README.md index f7c3730a79..7b4f90ef20 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ ![Issues](https://img.shields.io/github/issues-raw/Modrinth/code?color=c78aff&label=issues&style=for-the-badge) ![Pull Requests](https://img.shields.io/github/issues-pr-raw/Modrinth/code?color=c78aff&label=PRs&style=for-the-badge) ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=c78aff&label=contributors&style=for-the-badge) -![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/code/badge?style=flat&logoColor=white&color=c78aff&style=for-the-badge) +![Lines of Code](https://img.shields.io/endpoint?url=https://loctopus.creeperkatze.dev/github/modrinth/code/badge?style=flat&logoColor=white&color=c78aff&style=for-the-badge) ![Commit Activity](https://img.shields.io/github/commit-activity/m/Modrinth/code?color=c78aff&label=commits&style=for-the-badge) ![Last Commit](https://img.shields.io/github/last-commit/Modrinth/code?color=c78aff&label=last%20commit&style=for-the-badge) ## Modrinth Monorepo -Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains ![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/olympus/badge?logoColor=white&color=black&label=) lines of code and has ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=black&label=) contributors! +Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains ![Lines of Code](https://img.shields.io/endpoint?url=https://loctopus.creeperkatze.dev/github/modrinth/code/badge%3Fformat%3Dhuman&logoColor=white&color=black&label=) lines of code and has ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=black&label=) contributors! If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app). 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 c84a6c74ed..6946f2ba5b 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,21 +142,37 @@ 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 ROUTE_SUSPENSE_TIMEOUT_MS = 60_000 const credentials = ref() +let credentialsRefreshId = 0 const sidebarToggled = ref(true) const unsubscribeSidebarToggle = themeStore.$subscribe(() => { sidebarToggled.value = !themeStore.toggleSidebar @@ -201,12 +226,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, @@ -230,9 +258,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')), @@ -243,7 +292,7 @@ providePageContext({ themeStore.getFeatureFlag('server_ram_as_bytes_always_on'), ), }, - openExternalUrl: (url) => openUrl(url), + openExternalUrl: (url) => void openUrl(url), }) provideModalBehavior({ noblur: computed(() => !themeStore.advancedRendering), @@ -266,8 +315,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', () => { @@ -312,6 +363,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) @@ -325,6 +382,7 @@ onUnmounted(async () => { unsubscribeSidebarToggle() clearDelayedUpdatePopup() + await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -349,8 +407,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, @@ -420,7 +595,7 @@ async function setupApp() { addNotification({ title: 'Warning', text: e.message, - type: 'warn', + type: 'warning', }), ) @@ -469,12 +644,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) @@ -523,6 +692,7 @@ router.beforeEach(() => { routerToken = loading.begin() }) router.afterEach((to, from, failure) => { + updateHistoryNavigationState() trackEvent('PageView', { path: to.path, fromPath: from.path, @@ -654,6 +824,7 @@ watch(stateInitialized, (ready) => { const error = useError() const errorModal = ref() const minecraftAuthErrorModal = ref() +const minecraftRequiredModal = ref() const contentInstall = createContentInstall({ router, handleError }) provideContentInstall(contentInstall) @@ -703,9 +874,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) { @@ -713,8 +887,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) { @@ -731,23 +909,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 ( @@ -759,27 +949,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) { @@ -806,19 +1000,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) @@ -883,34 +1086,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) { @@ -921,12 +1142,13 @@ async function handleCommand(e) { if (e.path.endsWith('.mrpack')) { const location = { type: 'fromFile', path: e.path } const preview = await install_get_modpack_preview(location).catch(handleError) - if (preview?.unknownFile) { + if (preview?.unknownFile || preview?.externalFilesInModpack.length > 0) { const splitPath = e.path.split(/[\\/]/) const fileName = splitPath ? splitPath[splitPath.length - 1] : e.path unknownPackWarningModal.value?.show( () => install_create_modpack_instance(location).then(() => undefined), fileName, + preview.externalFilesInModpack, ) } else { await install_create_modpack_instance(location).catch(handleError) @@ -936,6 +1158,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) { @@ -943,6 +1168,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) @@ -1300,8 +1527,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) @@ -1328,115 +1558,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) @@ -1459,15 +1580,15 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="flex items-center gap-4 text-contrast font-semibold text-xl select-none cursor-default" > - Restarting... + {{ formatMessage(messages.restarting) }} - + - +
- + - - - - - + + -
@@ -1536,18 +1653,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! -

-
- - - - - - -
-
-
+