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