Compare commits

...
Author SHA1 Message Date
Thomas Camlong a84390818a fix(ci): repair docker build and consolidate CI/release workflows
The pnpm-fetch layer added in #6296 broke the release build: pnpm 11.10.0
applies patchedDependencies during `pnpm fetch`, before node_modules exists,
crashing with ENOENT. Install directly from the full context with the store
cache mounted instead.

Also audit and consolidate the workflows:
- standardize action majors (cache@v6, upload-artifact@v7, github-script@v9)
- provision Node from mise.toml via jdx/mise-action; drop .nvmrc
- rename deployment-docker-image.yml -> deployment-release.yml, reuse the
  shared setup action, and scope GITHUB_TOKEN permissions per job
- extract the duplicated beta/weekly release logic into a reusable
  deployment-release-pr.yml called by thin triggers
2026-07-13 19:27:11 +02:00
9 changed files with 242 additions and 215 deletions

No files matched your search

+3 -3
View File
@@ -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";
+15 -88
View File
@@ -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 }}``<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"
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' }}
+155
View File
@@ -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 }}\`<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"
@@ -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' }}
+17 -88
View File
@@ -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**.<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"
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' }}
+4 -9
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
24.18.0
+8 -6
View File
@@ -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'
+21 -6
View File
@@ -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