diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 81ba31419..97e3c1570 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -167,7 +167,7 @@ jobs:
- name: Cache Playwright browsers
if: '!env.ACT'
id: playwright-cache
- uses: actions/cache@v4
+ uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
@@ -185,7 +185,7 @@ jobs:
- name: Upload E2E artifacts
if: failure() && !env.ACT
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: playwright-report
path: e2e/test-results/
@@ -317,7 +317,7 @@ jobs:
- name: Delete GHCR package version
if: '!env.ACT'
- uses: actions/github-script@v8
+ uses: actions/github-script@v9
with:
script: |
const packageName = "homarr-test";
diff --git a/.github/workflows/deployment-beta-release.yml b/.github/workflows/deployment-beta-release.yml
index ba3bde3a2..9027ae0c4 100644
--- a/.github/workflows/deployment-beta-release.yml
+++ b/.github/workflows/deployment-beta-release.yml
@@ -14,91 +14,18 @@ permissions:
pull-requests: write
jobs:
- 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 }}``
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"
+ 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' }}
diff --git a/.github/workflows/deployment-release-pr.yml b/.github/workflows/deployment-release-pr.yml
new file mode 100644
index 000000000..529202862
--- /dev/null
+++ b/.github/workflows/deployment-release-pr.yml
@@ -0,0 +1,155 @@
+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 }}\`
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"
diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-release.yml
similarity index 93%
rename from .github/workflows/deployment-docker-image.yml
rename to .github/workflows/deployment-release.yml
index 9ea0c2a09..2e4e3d5f3 100644
--- a/.github/workflows/deployment-docker-image.yml
+++ b/.github/workflows/deployment-release.yml
@@ -15,9 +15,9 @@ on:
default: true
description: Send notifications
+# Least-privilege default; each job widens only what it needs.
permissions:
- contents: write # Required to update package.json version
- packages: write # Required for pushing to GHCR
+ contents: read
env:
SKIP_ENV_VALIDATION: true
@@ -34,6 +34,8 @@ 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:
@@ -61,19 +63,9 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
- - uses: pnpm/action-setup@v6
+ - name: Setup
if: env.SKIP_RELEASE == 'false'
- - 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
+ uses: ./tooling/github/setup
- name: Run Semantic Release
if: env.SKIP_RELEASE == 'false'
env:
@@ -116,6 +108,9 @@ 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:
@@ -160,6 +155,9 @@ 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:
@@ -204,6 +202,8 @@ 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,6 +220,8 @@ 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'
@@ -235,6 +237,9 @@ 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' }}
diff --git a/.github/workflows/deployment-weekly-release.yml b/.github/workflows/deployment-weekly-release.yml
index cafa7b30d..5096cca9b 100644
--- a/.github/workflows/deployment-weekly-release.yml
+++ b/.github/workflows/deployment-weekly-release.yml
@@ -17,91 +17,20 @@ permissions:
pull-requests: write
jobs:
- 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**.
Manual action may be required for major bumps.
Detected change to be ``${{ steps.semver.outputs.bump }}``
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"
+ 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**.
Manual action may be required for major bumps.
"
+ 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' }}
diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml
index bb4364596..0195842d3 100644
--- a/.github/workflows/update-contributors.yml
+++ b/.github/workflows/update-contributors.yml
@@ -15,9 +15,6 @@ jobs:
update-contributors:
runs-on: ubuntu-latest
timeout-minutes: 2
- strategy:
- matrix:
- node-version: [22]
steps:
- name: Obtain token
id: obtainToken
@@ -33,13 +30,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- - 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"
+ - name: Install Node with mise
+ uses: jdx/mise-action@v4
+ # 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
diff --git a/.nvmrc b/.nvmrc
deleted file mode 100644
index ca5c35005..000000000
--- a/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.18.0
diff --git a/Dockerfile b/Dockerfile
index ca493aa3c..5c2a45ee3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-# syntax=docker/dockerfile:1.10
+# syntax=docker/dockerfile:1.25
FROM node:24.18.0-alpine AS base
@@ -7,14 +7,16 @@ WORKDIR /app
RUN apk add --no-cache libc6-compat curl bash && apk update
RUN corepack enable pnpm
-COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
-COPY patches ./patches
-RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
- pnpm config set store-dir /pnpm/store && pnpm fetch
+# 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 . .
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
- pnpm install --recursive --offline --frozen-lockfile
+ pnpm config set store-dir /pnpm/store && \
+ pnpm install --recursive --frozen-lockfile
ARG SKIP_ENV_VALIDATION='true'
ARG CI='true'
diff --git a/tooling/github/setup/action.yml b/tooling/github/setup/action.yml
index 4f8330014..d66e7fb04 100644
--- a/tooling/github/setup/action.yml
+++ b/tooling/github/setup/action.yml
@@ -4,11 +4,26 @@ description: "Common setup steps for Actions"
runs:
using: composite
steps:
- - uses: pnpm/action-setup@v6
- - uses: actions/setup-node@v6
- with:
- node-version: 24.18.0
- cache: "pnpm"
+ # 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
- - shell: bash
+ - 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
+ with:
+ path: ${{ env.PNPM_STORE_PATH }}
+ key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ pnpm-store-${{ runner.os }}-
+
+ - name: Install dependencies
+ shell: bash
run: pnpm install --frozen-lockfile