Compare commits

..
Author SHA1 Message Date
ajnart 8c1e72819a style: auto-fix lint and format 2026-07-15 09:01:00 +00:00
ajnart a0fb7553a5 fix(widgets): restore per-widget refetch intervals lost in v1.69.0 refactor
The v1.69.0 caching refactor (f19033e0d) replaced the old Redis pub/sub
push mechanism with a global 30s client-side polling interval. This
caused widgets that previously refreshed every 5s (e.g. health-monitoring,
Dashdot, dns-hole) to only refresh every 30s.

Adds a widgetRefetchIntervalOverrides map in query-cache.ts that maps
tRPC path prefixes to refetch intervals matching the pre-v1.69.0
server-side cacheDuration values. The TRPC provider applies these
overrides via setQueryDefaults after the global default.

Also restores server-side cacheTtlMs on all request handlers to prevent
duplicate upstream API calls between client polls, and adds cacheTtlMs
support to createWidgetRequestHandler.

Beszel handlers are left untouched as they have their own cache handling.

Fixes #6268
2026-07-15 10:59:38 +02:00
homarr-renovate[bot] a0034b2cfc chore(deps): update dependency @immich/sdk to ^3.0.2 (#6339)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 21:01:58 +00:00
homarr-renovate[bot] 4a297180fd chore(deps): update dependency lucide-react to ^1.24.0 (#6334)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 19:23:26 +00:00
Thomas 5fa3a3313a Merge pull request #6337 from homarr-labs/fix/ts7-nextjs-build
fix(docker): pin TypeScript to v6 to fix Next.js 16.2.10 build in CI
2026-07-14 18:11:38 +02:00
Thomas Camlong 29ee87c4aa fix(docker): pin typescript to v6 via alias to fix Next.js 16.2.10 build 2026-07-14 18:11:15 +02:00
Thomas 94a247e24a fix(docker): remove --offline flag from pnpm install step (#6335) 2026-07-14 17:19:51 +02:00
homarr-renovate[bot] 6d37710fec chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.1.4 (#6333)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 11:26:55 +00:00
homarr-renovate[bot] c016beb2e5 chore(deps): update dependency vite to ^8.1.4 (#6332)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 09:06:17 +00:00
homarr-renovate[bot] f9e1b85c6a chore(deps): update dependency posthog-node to ^5.40.0 (#6314)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 06:14:41 +00:00
homarr-renovate[bot] 38ba19e665 chore(deps): update dependency oxlint to ^1.73.0 (#6306)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 03:22:16 +00:00
homarr-renovate[bot] f55c99d2d6 chore(deps): update dependency posthog-js to ^1.399.0 (#6331)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 00:00:38 +00:00
homarr-renovate[bot] c5e8b83d8a chore(deps): update vitest monorepo to ^4.1.10 (#6286)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 23:01:00 +00:00
homarr-renovate[bot] d52d9cfed2 chore(deps): update dependency posthog-js to ^1.398.7 (#6319)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 21:58:26 +00:00
Thomas 93325642aa Merge pull request #6324 from homarr-labs/renovate/major-github-artifact-actions 2026-07-13 22:12:32 +02:00
Thomas 165e94abbc Merge pull request #6328 from homarr-labs/fix-pnpm-fetch-build 2026-07-13 22:12:11 +02:00
homarr-renovate[bot] 2e3127e766 chore(deps): update docker/dockerfile docker tag to v1.25 (#6321) 2026-07-13 22:10:35 +02:00
Thomas d37326d905 Merge pull request #6322 from homarr-labs/renovate/actions-cache-6-x 2026-07-13 22:09:57 +02:00
homarr-renovate[bot] e51e39ff38 chore(deps): update dependency typescript to v7 (#6325) 2026-07-13 22:09:30 +02:00
homarr-renovate[bot] 6c075b6d74 chore(deps): update dependency mysql2 to v3.22.6 (#6310)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 18:45:08 +00:00
ajnart b2f63c6e0f fix(docker): drop --frozen-lockfile from install step per pnpm fetch docs
The pnpm fetch docs recommend `pnpm install -r --offline` without
--frozen-lockfile. The --offline flag already enforces no registry
access, making --frozen-lockfile redundant.

https://pnpm.io/cli/fetch
2026-07-13 20:24:59 +02:00
ajnart 8c2d73e8af fix(docker): resolve pnpm fetch crash with patchedDependencies
PR #6296 introduced pnpm fetch in the Dockerfile for Docker cache
optimization. However, pnpm fetch crashes with ENOENT when
patchedDependencies are configured with nodeLinker: hoisted because
applyPatchToDir tries to chdir into node_modules/<pkg> which doesn't
exist during fetch (only the content-addressable store is populated).

Fix: temporarily switch nodeLinker to isolated during pnpm fetch so
patches apply inside the virtual store (node_modules/.pnpm/...) which
IS created by pnpm fetch. Restore hoisted before the install step so
the final node_modules layout stays flat.

Refs: pnpm/pnpm#5268, pnpm/pnpm#8726
2026-07-13 20:06:28 +02:00
homarr-renovate[bot] 1c0ff8930e chore(deps): update actions/upload-artifact action to v7 2026-07-13 16:39:27 +00:00
homarr-renovate[bot] e2b8355639 chore(deps): update actions/cache action to v6 2026-07-13 16:39:10 +00:00
47 changed files with 891 additions and 902 deletions

No files matched your search

+1 -1
View File
@@ -317,7 +317,7 @@ jobs:
- name: Delete GHCR package version
if: '!env.ACT'
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const packageName = "homarr-test";
+88 -15
View File
@@ -14,18 +14,91 @@ permissions:
pull-requests: write
jobs:
release:
# Permissions must be set on the calling job: a reusable workflow receives
# the default GITHUB_TOKEN permissions unless the caller job grants them here.
permissions:
contents: write
pull-requests: write
uses: ./.github/workflows/deployment-release-pr.yml
secrets: inherit
with:
release-label: "Beta release"
version-suffix: "-beta"
semver-branch: next
head: next
base: beta
send-notifications: ${{ github.event.inputs.send-notifications == 'true' }}
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: next
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}-beta (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to beta
MERGE_COMMIT_MESSAGE: "chore(release): release ${{ steps.semver.outputs.next }}-beta"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The beta release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
@@ -15,9 +15,9 @@ on:
default: true
description: Send notifications
# Least-privilege default; each job widens only what it needs.
permissions:
contents: read
contents: write # Required to update package.json version
packages: write # Required for pushing to GHCR
env:
SKIP_ENV_VALIDATION: true
@@ -34,8 +34,6 @@ jobs:
name: Create tag and release
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # commit package.json/changelog, push tags, update dev/next
env:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
@@ -63,9 +61,19 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
- name: Setup
- uses: pnpm/action-setup@v6
if: env.SKIP_RELEASE == 'false'
uses: ./tooling/github/setup
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.18.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
if: env.SKIP_RELEASE == 'false'
run: |
pnpm install
- name: Run Semantic Release
if: env.SKIP_RELEASE == 'false'
env:
@@ -108,9 +116,6 @@ jobs:
needs: release
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read # checkout the repository
packages: write # push image by digest to GHCR
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
@@ -155,9 +160,6 @@ jobs:
needs: release
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read # checkout the repository
packages: write # push image by digest to GHCR
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
@@ -202,8 +204,6 @@ jobs:
needs: [release, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # upload build artifacts as release assets
steps:
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
@@ -220,8 +220,6 @@ jobs:
needs: [release, build-arm64]
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
permissions:
contents: write # upload build artifacts as release assets
steps:
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
@@ -237,9 +235,6 @@ jobs:
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # mark GitHub release as published (undraft)
packages: write # create multi-arch image tags in GHCR
env:
NEXT_VERSION: ${{ needs.release.outputs.version }}
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
-155
View File
@@ -1,155 +0,0 @@
name: "[Deployment] Release PR (reusable)"
# Reusable workflow that opens a release PR from `head` into `base`, auto-approves
# it and (for non-major bumps) merges it. Used by the beta and weekly release
# workflows, which differ only in branch, version suffix and notification wording.
on:
workflow_call:
inputs:
release-label:
description: 'Human label used in notifications, e.g. "Beta release"'
required: true
type: string
title-prefix:
description: 'Text inserted before "release" in the PR title/commit, e.g. "automatic "'
required: false
default: ""
type: string
version-suffix:
description: 'Suffix appended to the computed version, e.g. "-beta"'
required: false
default: ""
type: string
body-prefix:
description: "Extra HTML prepended to the PR body"
required: false
default: ""
type: string
semver-branch:
description: "Branch used to compute the next version"
required: true
type: string
head:
description: "PR head branch"
required: true
type: string
base:
description: "PR base branch"
required: true
type: string
send-notifications:
description: "Whether to send Discord notifications"
required: false
default: true
type: boolean
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
permissions:
contents: write
pull-requests: write
steps:
- name: Discord notification
if: inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "${{ inputs.release-label }} has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: ${{ inputs.semver-branch }}
- name: Create pull request
id: create-pull-request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: "${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
run: >-
gh pr create
--title "chore(release): ${{ inputs.title-prefix }}release ${VERSION}"
--body "${{ inputs.body-prefix }}Detected change to be \`${{ steps.semver.outputs.bump }}\`<br/>Bump version from \`${{ steps.semver.outputs.current }}\` to \`${VERSION}\`"
--base ${{ inputs.base }}
--head ${{ inputs.head }}
--label automerge
- name: Discord notification
if: inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to the base branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for release branches
MERGE_COMMIT_MESSAGE: "chore(release): ${{ inputs.title-prefix }}release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
- name: Discord Fail Notification
if: failure() && inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The ${{ inputs.release-label }} workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+88 -17
View File
@@ -17,20 +17,91 @@ permissions:
pull-requests: write
jobs:
release:
# Permissions must be set on the calling job: a reusable workflow receives
# the default GITHUB_TOKEN permissions unless the caller job grants them here.
permissions:
contents: write
pull-requests: write
uses: ./.github/workflows/deployment-release-pr.yml
secrets: inherit
with:
release-label: "Automatic release"
title-prefix: "automatic "
body-prefix: "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>"
semver-branch: dev
head: dev
base: main
# Scheduled runs have no inputs, so notifications stay off unless manually dispatched.
send-notifications: ${{ github.event.inputs.send-notifications == 'true' }}
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: dev
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to main
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+9 -4
View File
@@ -15,6 +15,9 @@ jobs:
update-contributors:
runs-on: ubuntu-latest
timeout-minutes: 2
strategy:
matrix:
node-version: [22]
steps:
- name: Obtain token
id: obtainToken
@@ -30,11 +33,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Install Node with mise
uses: jdx/mise-action@v4
- uses: pnpm/action-setup@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
# This script only uses Node built-ins (fs/promises), so no dependency
# install is needed.
- name: Run update script
run: node ./scripts/update-contributors.mjs
+1
View File
@@ -0,0 +1 @@
24.18.0
+18 -8
View File
@@ -7,16 +7,26 @@ WORKDIR /app
RUN apk add --no-cache libc6-compat curl bash && apk update
RUN corepack enable pnpm
# NOTE: We intentionally do NOT use `pnpm fetch` here. pnpm 11.10.0 applies
# `patchedDependencies` patches during the fetch phase, before packages are
# unpacked into node_modules, which crashes with ENOENT (chdir into a
# not-yet-existing node_modules path). Installing directly from the full
# context with the store cache mounted keeps builds fast without that break.
COPY . .
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY patches ./patches
# Workaround for pnpm/pnpm#5268: pnpm fetch crashes when patchedDependencies
# are configured with nodeLinker: hoisted. The applyPatchToDir function tries
# to chdir into node_modules/<pkg> which doesn't exist during fetch (only the
# content-addressable store is populated). By temporarily switching to the
# isolated linker, patches apply inside the virtual store (node_modules/.pnpm/...)
# which IS created by pnpm fetch. The original hoisted linker is restored
# before the install step so the final node_modules layout stays flat.
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm config set store-dir /pnpm/store && \
pnpm install --recursive --frozen-lockfile
sed -i 's/nodeLinker: hoisted/nodeLinker: isolated/' pnpm-workspace.yaml && \
pnpm fetch && \
sed -i 's/nodeLinker: isolated/nodeLinker: hoisted/' pnpm-workspace.yaml
COPY . .
# Follow the pnpm fetch pattern from https://pnpm.io/cli/fetch
# --frozen-lockfile is omitted as recommended by the pnpm fetch docs
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --recursive
ARG SKIP_ENV_VALIDATION='true'
ARG CI='true'
@@ -29,6 +29,7 @@ import {
queryCacheDefaultGcTimeMs,
queryCacheDefaultRefetchIntervalMs,
queryCacheDefaultStaleTimeMs,
widgetRefetchIntervalOverrides,
} from "@homarr/api/query-cache";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "@homarr/common/env";
@@ -90,6 +91,9 @@ export function TRPCReactProvider(props: PropsWithChildren) {
},
});
client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs });
for (const [path, interval] of Object.entries(widgetRefetchIntervalOverrides)) {
client.setQueryDefaults([path.split(".")], { refetchInterval: interval });
}
return client;
});
+38
View File
@@ -7,6 +7,44 @@ export const queryCacheMaxValueBytes = 1024 * 1024;
export const queryCacheStoragePrefix = "homarr-widget-query";
export const queryCacheBuster = "v2-superjson";
export const widgetRefetchIntervalOverrides: Record<string, number> = {
"widget.healthMonitoring": 5_000,
"widget.dnsHole": 5_000,
"widget.downloads": 5_000,
"widget.mediaServer": 5_000,
"widget.tracearr": 5_000,
"widget.firewall.getFirewallCpuStatus": 5_000,
"widget.firewall.getFirewallMemoryStatus": 15_000,
"widget.firewall.getFirewallInterfacesStatus": 30_000,
"widget.firewall.getFirewallVersionStatus": 3_600_000,
"widget.anchorNotes": 15_000,
"widget.coolify": 30_000,
"widget.umami.getActiveVisitors": 30_000,
"widget.umami": 300_000,
"widget.calendar": 60_000,
"widget.mediaRequests": 60_000,
"widget.smartHome": 60_000,
"widget.ups": 60_000,
"widget.uptimeKuma": 60_000,
"widget.weather": 60_000,
"widget.timetable": 60_000,
"widget.indexerManager": 300_000,
"widget.mediaRelease": 300_000,
"widget.mediaTranscoding": 300_000,
"widget.networkController": 300_000,
"widget.notifications": 300_000,
"widget.rssFeed": 300_000,
"widget.stockPrice": 300_000,
"widget.minecraft": 300_000,
"widget.vpn": 300_000,
"widget.archiveTeamWarrior": 300_000,
"widget.audiobookshelf": 600_000,
"widget.navidrome": 600_000,
"widget.speedtestTracker": 600_000,
"widget.immich": 900_000,
"widget.paperlessNgx": 900_000,
};
let activeQueryCacheBoardId: string | null = null;
export const setActiveQueryCacheBoardId = (boardId: string | null) => {
@@ -12,6 +12,7 @@ export const anchorNotesListRequestHandler = createIntegrationRequestHandler<
const instance = await createIntegrationAsync(integration);
return instance.listNotesAsync(input);
},
cacheTtlMs: 15_000,
});
export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNote, "anchor", { noteId: string }>({
@@ -19,4 +20,5 @@ export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNo
const instance = await createIntegrationAsync(integration);
return instance.getNoteAsync(input.noteId);
},
cacheTtlMs: 15_000,
});
@@ -13,4 +13,5 @@ export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatusAsync();
},
cacheTtlMs: 300_000,
});
@@ -12,4 +12,5 @@ export const audiobookshelfRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheTtlMs: 600_000,
});
+1
View File
@@ -23,4 +23,5 @@ export const calendarMonthRequestHandler = createIntegrationRequestHandler<
input.showUnmonitored,
);
},
cacheTtlMs: 60_000,
});
+1
View File
@@ -12,4 +12,5 @@ export const coolifyRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getInstanceInfoAsync();
},
cacheTtlMs: 30_000,
});
+1
View File
@@ -13,4 +13,5 @@ export const dnsHoleRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSummaryAsync();
},
cacheTtlMs: 5_000,
});
+1
View File
@@ -177,6 +177,7 @@ export const dockerContainersRequestHandler = createWidgetRequestHandler({
}
return await getContainersWithStatsAsync();
},
cacheTtlMs: 20_000,
});
const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image);
@@ -13,4 +13,5 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync(input);
},
cacheTtlMs: 5_000,
});
@@ -69,4 +69,5 @@ export const duckDuckGoBangsRequestHandler = createRequestHandler({
return normalized;
},
cacheTtlMs: 86_400_000,
});
+4
View File
@@ -18,6 +18,7 @@ export const firewallCpuRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return integrationInstance.getFirewallCpuAsync();
},
cacheTtlMs: 5_000,
});
export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
@@ -29,6 +30,7 @@ export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallMemoryAsync();
},
cacheTtlMs: 15_000,
});
export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
@@ -40,6 +42,7 @@ export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallInterfacesAsync();
},
cacheTtlMs: 30_000,
});
export const firewallVersionRequestHandler = createIntegrationRequestHandler<
@@ -51,4 +54,5 @@ export const firewallVersionRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallVersionAsync();
},
cacheTtlMs: 3_600_000,
});
@@ -13,6 +13,7 @@ export const systemInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSystemInfoAsync();
},
cacheTtlMs: 5_000,
});
export const clusterInfoRequestHandler = createIntegrationRequestHandler<
@@ -24,4 +25,5 @@ export const clusterInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClusterInfoAsync();
},
cacheTtlMs: 5_000,
});
+3
View File
@@ -13,6 +13,7 @@ export const immichStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getServerStatsAsync();
},
cacheTtlMs: 900_000,
});
export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
@@ -28,6 +29,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumsAsync();
},
cacheTtlMs: 900_000,
});
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
@@ -39,4 +41,5 @@ export const immichAlbumRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumAsync(input.albumId);
},
cacheTtlMs: 900_000,
});
@@ -13,4 +13,5 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getIndexersAsync();
},
cacheTtlMs: 300_000,
});
@@ -2,6 +2,7 @@ import { createRequestHandler } from "./request-handler";
interface Options<TData, TInput extends Record<string, unknown>> {
requestAsync: (input: TInput) => Promise<TData>;
cacheTtlMs?: number;
}
export const createWidgetRequestHandler = <TData, TInput extends Record<string, unknown>>(
@@ -10,5 +11,6 @@ export const createWidgetRequestHandler = <TData, TInput extends Record<string,
handler: (widgetOptions: TInput) =>
createRequestHandler<TData, TInput>({
requestAsync: options.requestAsync,
cacheTtlMs: options.cacheTtlMs,
}).handler(widgetOptions),
});
@@ -13,4 +13,5 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMediaReleasesAsync();
},
cacheTtlMs: 300_000,
});
@@ -16,4 +16,5 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
users: await integrationInstance.getUsersAsync(),
};
},
cacheTtlMs: 60_000,
});
@@ -15,4 +15,5 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
},
cacheTtlMs: 5_000,
});
@@ -17,6 +17,7 @@ export const mediaTranscodingRequestHandler = createIntegrationRequestHandler<
statistics: await integrationInstance.getStatisticsAsync(),
};
},
cacheTtlMs: 300_000,
});
export interface MediaTranscoding {
@@ -14,6 +14,7 @@ export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
);
return responseSchema.parse(await response.json());
},
cacheTtlMs: 300_000,
});
const responseSchema = z
@@ -12,4 +12,5 @@ export const navidromeRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheTtlMs: 600_000,
});
@@ -13,4 +13,5 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNetworkSummaryAsync();
},
cacheTtlMs: 300_000,
});
@@ -13,4 +13,5 @@ export const notificationsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync();
},
cacheTtlMs: 300_000,
});
@@ -12,4 +12,5 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatsAsync();
},
cacheTtlMs: 900_000,
});
@@ -28,6 +28,7 @@ export const rssFeedsRequestHandler = createWidgetRequestHandler({
entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [],
};
},
cacheTtlMs: 300_000,
});
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
@@ -18,4 +18,5 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
return result.data.state;
},
cacheTtlMs: 60_000,
});
@@ -12,4 +12,5 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheTtlMs: 600_000,
});
@@ -39,6 +39,7 @@ export const fetchStockPriceHandler = createWidgetRequestHandler({
shortName: firstResult.meta.shortName,
};
},
cacheTtlMs: 300_000,
});
const dataSchema = z
+1
View File
@@ -12,4 +12,5 @@ export const tracearrRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheTtlMs: 5_000,
});
+6
View File
@@ -12,6 +12,7 @@ export const umamiRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName);
},
cacheTtlMs: 300_000,
});
export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<string[], "umami", { websiteId: string }>({
@@ -19,6 +20,7 @@ export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<str
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getEventNamesAsync(input.websiteId);
},
cacheTtlMs: 300_000,
});
export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
@@ -30,6 +32,7 @@ export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit);
},
cacheTtlMs: 300_000,
});
export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
@@ -41,6 +44,7 @@ export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit);
},
cacheTtlMs: 300_000,
});
export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
@@ -52,6 +56,7 @@ export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames);
},
cacheTtlMs: 300_000,
});
export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler<
@@ -63,4 +68,5 @@ export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getActiveVisitorsAsync(input.websiteId);
},
cacheTtlMs: 30_000,
});
+1
View File
@@ -13,4 +13,5 @@ export const upsSummariesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getUpsSummariesAsync();
},
cacheTtlMs: 60_000,
});
@@ -12,4 +12,5 @@ export const uptimeKumaRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheTtlMs: 60_000,
});
+1
View File
@@ -25,4 +25,5 @@ export const vpnSummaryHandler = createIntegrationRequestHandler<
return null;
}
},
cacheTtlMs: 300_000,
});
+1
View File
@@ -31,6 +31,7 @@ export const weatherRequestHandler = createWidgetRequestHandler({
}),
} satisfies Weather;
},
cacheTtlMs: 60_000,
});
const atLocationOutput = z.object({
+3 -9
View File
@@ -184,12 +184,8 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const t = useScopedI18n("docker");
const isTiny = width <= 256;
const {
data,
refetch,
isFetching,
} = clientApi.docker.getContainers.useQuery(undefined, {
refetchInterval: 30_000,
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery(undefined, {
refetchInterval: 20_000,
});
const containers = data?.containers ?? [];
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
@@ -304,9 +300,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
{t("table.totalMemory", { memory: formatBytes(totals.memory) })}
</Text>
<Tooltip
label={t("table.refresh.lastUpdated", { when: relativeTime })}
>
<Tooltip label={t("table.refresh.lastUpdated", { when: relativeTime })}>
<ActionIcon
size="sm"
variant="transparent"
+564 -642
View File
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -18,7 +18,7 @@ catalog:
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
"@immich/sdk": ^3.0.1
"@immich/sdk": ^3.0.2
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@modelcontextprotocol/sdk": 1.29.0
@@ -93,11 +93,11 @@ catalog:
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.398.2
posthog-node: ^5.39.2
posthog-js: ^1.399.0
posthog-node: ^5.40.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
"@vitest/ui": ^4.1.9
"@vitest/coverage-v8": ^4.1.10
"@vitest/ui": ^4.1.10
"@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0
@@ -139,20 +139,20 @@ catalog:
jsonpath-plus: ^10.4.0
ldapts: 8.1.8
lodash.clonedeep: ^4.5.0
lucide-react: ^1.23.0
lucide-react: ^1.24.0
"mantine-contextmenu": ^9.3.1
mantine-datatable: ^9.3.1
mantine-react-table: npm:mantine-react-table-open@9.0.3
mcp-handler: ^1.1.0
maria2: ^0.4.1
mysql2: 3.22.5
mysql2: 3.22.6
next: 16.2.10
next-auth: 5.0.0-beta.31
next-intl: 4.13.1
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.58.0
oxlint: ^1.72.0
oxlint: ^1.73.0
pg: ^8.22.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -177,13 +177,13 @@ catalog:
tsdav: ^2.3.0
tsx: 4.20.4
turbo: ^2.10.4
typescript: ^6.0.3
vite: ^8.1.2
typescript: "npm:@typescript/typescript6@^6.0.2"
vite: ^8.1.4
undici: 7.28.0
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
vite-tsconfig-paths: ^6.1.1
vitest: ^4.1.9
vitest: ^4.1.10
winston: 3.19.0
ws: ^8.21.0
xml2js: ^0.6.2
@@ -229,7 +229,7 @@ overrides:
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.3"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.3"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.1.3"
"vite@>=5.0.0 <=5.4.18": ">=8.1.4"
patchedDependencies:
"@types/node-unifi": "patches/@types__node-unifi.patch"
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
+5 -20
View File
@@ -4,26 +4,11 @@ description: "Common setup steps for Actions"
runs:
using: composite
steps:
# Node (and any other tools) are provisioned from mise.toml, the single
# source of truth for tool versions.
- name: Install tools with mise
uses: jdx/mise-action@v4
- name: Install pnpm
uses: pnpm/action-setup@v6
- name: Locate pnpm store
shell: bash
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"
- name: Cache pnpm store
uses: actions/cache@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
path: ${{ env.PNPM_STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
node-version: 24.18.0
cache: "pnpm"
- name: Install dependencies
shell: bash
- shell: bash
run: pnpm install --frozen-lockfile