From ba7718bde5c36ec75be208ee9eb56bb0e6b69f91 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 13 Jul 2026 18:32:48 +0200 Subject: [PATCH] feat: rework the ci (#6296) --- .dockerignore | 7 + .github/act/.gitignore | 2 + .github/act/.secrets.example | 4 + .github/act/README.md | 36 ++ .github/workflows/autofix.yml | 48 +++ .github/workflows/ci.yml | 347 ++++++++++++++++++ .github/workflows/code-quality.yml | 165 --------- .github/workflows/deployment-docker-image.yml | 10 + Dockerfile | 29 +- package.json | 12 +- scripts/ci/act.sh | 97 +++++ turbo.json | 3 +- 12 files changed, 579 insertions(+), 181 deletions(-) create mode 100644 .github/act/.gitignore create mode 100644 .github/act/.secrets.example create mode 100644 .github/act/README.md create mode 100644 .github/workflows/autofix.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/code-quality.yml create mode 100644 scripts/ci/act.sh diff --git a/.dockerignore b/.dockerignore index 74c010b43..521c55109 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,14 @@ npm-debug.log README.md .next .turbo +.pnpm-store +**/.pnpm-store +coverage +**/coverage +**/.docusaurus +**/build .git +.github dev .build e2e diff --git a/.github/act/.gitignore b/.github/act/.gitignore new file mode 100644 index 000000000..bf697bacf --- /dev/null +++ b/.github/act/.gitignore @@ -0,0 +1,2 @@ +.secrets +!.secrets.example diff --git a/.github/act/.secrets.example b/.github/act/.secrets.example new file mode 100644 index 000000000..377f2b6cf --- /dev/null +++ b/.github/act/.secrets.example @@ -0,0 +1,4 @@ +TURBO_API= +TURBO_TEAM= +TURBO_TOKEN= +TURBO_REMOTE_CACHE_SIGNATURE_KEY= diff --git a/.github/act/README.md b/.github/act/README.md new file mode 100644 index 000000000..8bac83dfa --- /dev/null +++ b/.github/act/README.md @@ -0,0 +1,36 @@ +# Running CI locally + +Install [Act](https://nektosact.com/installation/index.html) and ensure Docker is running, then use: + +```bash +pnpm ci:act:fast +pnpm ci:act:docker +pnpm ci:act:all +pnpm ci:act:cleanup +``` + +`fast` runs the quality gate. `docker` and `all` run the gate followed by the single container build, E2E suite, and memory regression test. `cleanup` validates the manual cleanup path without calling GitHub. The wrapper supports regular clones and Git worktrees, including non-default Docker contexts. Linux dependencies are kept in Docker volumes instead of overwriting host `node_modules`. The first run downloads the Act runner and Playwright browser dependencies. + +Remote caching is optional locally. Copy `.github/act/.secrets.example` to `.github/act/.secrets`, populate it with rotated credentials, and run with: + +```bash +ACT_SECRET_FILE=.github/act/.secrets pnpm ci:act:all +``` + +Act never publishes or deletes GHCR images. The secret file is ignored by Git. + +## PR preview images + +Successful non-draft pull requests from this repository publish an amd64 image: + +```bash +docker pull ghcr.io/homarr-labs/homarr-test:pr- +``` + +The tag is replaced when the pull request is updated and deleted when it closes. Production Homarr images remain the supported multi-architecture releases. The `homarr-test` package must be configured as public in GHCR for unauthenticated testers. + +## Dokploy preview cache + +Keep `needs-demo` as the Dokploy preview label. Under the application's **Preview Build-time Secrets**, configure `TURBO_API`, `TURBO_TEAM`, `TURBO_TOKEN`, and `TURBO_REMOTE_CACHE_SIGNATURE_KEY`. Dokploy passes these to the Dockerfile as BuildKit secrets. Do not configure the token or signature key as Docker build arguments. + +Rotate any credentials that have appeared in chat, logs, or committed configuration before enabling the cache. diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..a1b594223 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,48 @@ +name: Auto-fix lint and format + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: ["*"] + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + autofix: + name: Auto-fix + if: github.event.pull_request.head.repo.id == github.repository_id + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + persist-credentials: false + + - name: Setup + uses: ./tooling/github/setup + + - name: Get changed files + id: changed + env: + BASE_REF: ${{ github.base_ref }} + run: | + git diff --name-only --diff-filter=ACMR "origin/${BASE_REF}...HEAD" -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files + echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT + + - name: Auto-fix lint + format + if: steps.changed.outputs.count != '0' + run: | + xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true + xargs -r -a /tmp/changed-files pnpm exec oxfmt --write + + - name: Commit fixes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-fix lint and format" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..81ba31419 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,347 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: ["*"] + pull_request_target: + types: [closed] + branches: ["*"] + push: + branches: [main, dev, beta, next] + merge_group: + workflow_dispatch: + inputs: + operation: + description: Operation to run + required: true + default: validate + type: choice + options: + - validate + - publish + - cleanup + pr_number: + description: PR number for publish or cleanup + required: false + type: number + ref: + description: Git ref to validate or publish + required: false + type: string + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + FORCE_COLOR: 3 + SKIP_ENV_VALIDATION: true + TURBO_TELEMETRY_DISABLED: 1 + REGISTRY: ghcr.io + PREVIEW_IMAGE: ghcr.io/homarr-labs/homarr-test + +jobs: + fast-gate: + name: Fast gate + if: github.event_name != 'pull_request_target' && (github.event_name != 'workflow_dispatch' || inputs.operation != 'cleanup') + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + TURBO_API: ${{ secrets.TURBO_API }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || 'HEAD~1' }} + TURBO_SCM_HEAD: ${{ github.sha }} + TURBO_PLATFORM: linux/amd64 + steps: + - uses: actions/checkout@v7 + if: '!env.ACT' + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} + + - name: Validate manual inputs + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'publish' + env: + PR_NUMBER: ${{ inputs.pr_number }} + BUILD_REF: ${{ inputs.ref }} + run: | + test -n "$PR_NUMBER" || { echo "pr_number is required for publish"; exit 1; } + test -n "$BUILD_REF" || { echo "ref is required for publish"; exit 1; } + + - name: Setup + uses: ./tooling/github/setup + + - name: Copy environment defaults + run: cp .env.example .env + + - name: Check formatting in affected workspaces + run: pnpm turbo run format --affected --continue -- --check + + - name: Lint, typecheck, and build affected workspaces + run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful + + - name: Validate workspace configuration + run: pnpm lint:ws + + - name: Run unit tests + run: pnpm test + + - name: Report coverage + if: always() && !env.ACT + uses: davelosert/vitest-coverage-report-action@v2 + + container: + name: Container, E2E, and memory + needs: fast-gate + if: | + github.event_name == 'merge_group' || + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || + (github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish')) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + if: '!env.ACT' + with: + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} + + - name: Setup + uses: ./tooling/github/setup + + - name: Set up Docker Buildx + if: '!env.ACT' + uses: docker/setup-buildx-action@v4 + + - name: Build container once + if: '!env.ACT' + uses: docker/build-push-action@v7 + with: + context: . + network: host + platforms: linux/amd64 + load: true + push: false + tags: | + homarr-ci + homarr-e2e + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }} + cache-from: type=gha,scope=homarr-linux-amd64 + cache-to: type=gha,scope=homarr-linux-amd64,mode=max + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Build container once with Act + if: env.ACT + env: + TURBO_API: ${{ secrets.TURBO_API }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + run: | + secret_args=() + for secret in TURBO_API TURBO_TEAM TURBO_TOKEN TURBO_REMOTE_CACHE_SIGNATURE_KEY; do + if [ -n "${!secret:-}" ]; then + secret_args+=(--secret "id=${secret},env=${secret}") + fi + done + DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \ + --tag homarr-ci --tag homarr-e2e "${secret_args[@]}" . + + - name: Cache Playwright browsers + if: '!env.ACT' + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install Playwright Chromium + if: steps.playwright-cache.outputs.cache-hit != 'true' || env.ACT + run: pnpm exec playwright install --with-deps chromium + + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: pnpm exec playwright install-deps chromium + + - name: Run E2E tests + run: pnpm test:e2e + + - name: Upload E2E artifacts + if: failure() && !env.ACT + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: e2e/test-results/ + retention-days: 7 + + - name: Run memory regression test + env: + CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }} + SECRET_ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000 + run: | + docker run -d --name "$CONTAINER_NAME" -p 127.0.0.1::7575 \ + -e NO_EXTERNAL_CONNECTION=true \ + -e SECRET_ENCRYPTION_KEY="$SECRET_ENCRYPTION_KEY" \ + homarr-ci + PORT=$(docker port "$CONTAINER_NAME" 7575/tcp | sed 's/.*://') + [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid port mapping: '$PORT'"; exit 1; } + URL="http://127.0.0.1:${PORT}" + + healthy=false + for i in $(seq 1 30); do + if curl -sf "$URL/api/health/ready" >/dev/null 2>&1; then + echo "Container healthy after $((i * 2))s" + healthy=true + break + fi + sleep 2 + done + + if [ "$healthy" != "true" ]; then + echo "ERROR: Container never became healthy" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + fi + + warmup_ok=0 + for _ in $(seq 1 5); do + curl -sf "$URL/" >/dev/null 2>&1 && warmup_ok=$((warmup_ok + 1)) + sleep 2 + done + if [ "$warmup_ok" -eq 0 ]; then + echo "ERROR: All warmup requests failed" + docker logs "$CONTAINER_NAME" --tail 50 + exit 1 + fi + + docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" | grep -qx true || { + echo "ERROR: Container died before memory measurement" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + } + + RAW_MEMORY=$(docker stats --no-stream --format '{{.MemUsage}}' "$CONTAINER_NAME" | sed 's| /.*||') + MEMORY_MB=$(echo "$RAW_MEMORY" | awk ' + /GiB/ { gsub(/GiB/, ""); print $1 * 1024; next } + /MiB/ { gsub(/MiB/, ""); print $1; next } + /KiB/ { gsub(/KiB/, ""); print $1 / 1024; next } + { print $1 / 1048576 } + ') + + echo "Memory usage: ${MEMORY_MB} MB (${RAW_MEMORY})" + [[ "$MEMORY_MB" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { + echo "ERROR: non-numeric memory value: '$MEMORY_MB' (raw: '$RAW_MEMORY')" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + } + if awk "BEGIN { exit !($MEMORY_MB < 50) }"; then + echo "ERROR: Memory suspiciously low (${MEMORY_MB} MB) — container may have crashed" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + elif awk "BEGIN { exit !($MEMORY_MB > 700) }"; then + echo "ERROR: Memory usage ${MEMORY_MB} MB exceeds 700 MB" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + fi + + - name: Clean up memory test container + if: always() + env: + CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }} + run: docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + + - name: Determine preview tag + id: preview + if: | + !env.ACT && + ( + (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'workflow_dispatch' && inputs.operation == 'publish') + ) + env: + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + run: echo "tag=pr-${PR_NUMBER}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: steps.preview.outputs.tag != '' + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push preview image + if: steps.preview.outputs.tag != '' + env: + PREVIEW_TAG: ${{ steps.preview.outputs.tag }} + run: | + docker tag homarr-ci "${PREVIEW_IMAGE}:${PREVIEW_TAG}" + docker push "${PREVIEW_IMAGE}:${PREVIEW_TAG}" + + - name: Show Act publication result + if: env.ACT + run: echo "Act validated the publication path; external GHCR writes are disabled locally." + + cleanup: + name: Clean up preview image + if: github.event_name == 'pull_request_target' || (github.event_name == 'workflow_dispatch' && inputs.operation == 'cleanup') + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + packages: write + env: + IMAGE_TAG: pr-${{ github.event.pull_request.number || inputs.pr_number }} + steps: + - name: Validate cleanup input + if: github.event_name == 'workflow_dispatch' + env: + PR_NUMBER: ${{ inputs.pr_number }} + run: test -n "$PR_NUMBER" || { echo "pr_number is required for cleanup"; exit 1; } + + - name: Delete GHCR package version + if: '!env.ACT' + uses: actions/github-script@v8 + with: + script: | + const packageName = "homarr-test"; + const org = context.repo.owner; + const tag = process.env.IMAGE_TAG; + const versions = await github.paginate( + github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, + { package_type: "container", package_name: packageName, org, per_page: 100 }, + ); + const target = versions.find((version) => + version.metadata?.container?.tags?.includes(tag), + ); + if (!target) { + core.info(`No package version found with tag "${tag}"`); + return; + } + await github.rest.packages.deletePackageVersionForOrg({ + package_type: "container", + package_name: packageName, + org, + package_version_id: target.id, + }); + core.info(`Deleted package version ${target.id} (${tag})`); + + - name: Show Act cleanup result + if: env.ACT + run: echo "Act validated cleanup for ${IMAGE_TAG}; external GHCR writes are disabled locally." diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index 48d017cde..000000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: "[Quality] Code Analysis" -permissions: - contents: read - -on: - pull_request: - branches: ["*"] - push: - branches: ["main"] - merge_group: - -concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -env: - FORCE_COLOR: 3 - SKIP_ENV_VALIDATION: true - TURBO_API: ${{ secrets.TURBO_API }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }} - TURBO_SCM_HEAD: ${{ github.sha }} - -jobs: - lint-format-autofix: - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - ref: ${{ github.head_ref }} - - - name: Setup - uses: ./tooling/github/setup - - - name: Get changed files - id: changed - run: | - git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files - echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT - - - name: Auto-fix lint + format - if: steps.changed.outputs.count != '0' - run: | - xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true - xargs -r -a /tmp/changed-files pnpm exec oxfmt --write - - - name: Commit fixes - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: "style: auto-fix lint and format" - - lint: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Copy env - shell: bash - run: cp .env.example .env - - - name: Lint - run: pnpm turbo lint --affected && pnpm lint:ws - - format: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Format - run: pnpm turbo format --affected --continue - - typecheck: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Typecheck - run: pnpm turbo typecheck --affected - - test: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - - - name: Setup - uses: ./tooling/github/setup - - - name: Test - run: pnpm test - - - name: "Report Coverage" - # Set if: always() to also generate the report if tests are failing - # Only works if you set `reportOnFailure: true` in your vite config as specified above - if: always() - uses: davelosert/vitest-coverage-report-action@v2 - - e2e: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v7 - - name: Setup - uses: ./tooling/github/setup - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Build docker image - id: build-docker-image - uses: docker/build-push-action@v7 - with: - platforms: linux/amd64 - context: . - push: false - load: true - tags: homarr-e2e - network: host - cache-from: type=gha - cache-to: type=gha,mode=max - env: - SKIP_ENV_VALIDATION: true - - name: Install playwright browsers - run: pnpm exec playwright install chromium - - name: Run E2E Tests - shell: bash - run: pnpm test:e2e - - build: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Setup - uses: ./tooling/github/setup - - name: Copy env - shell: bash - run: cp .env.example .env - - name: Build - run: pnpm turbo build --affected diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml index 3ab3a758e..9ea0c2a09 100644 --- a/.github/workflows/deployment-docker-image.yml +++ b/.github/workflows/deployment-docker-image.yml @@ -148,6 +148,11 @@ jobs: platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} env: SKIP_ENV_VALIDATION: true build-arm64: @@ -187,6 +192,11 @@ jobs: platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} env: SKIP_ENV_VALIDATION: true extract-asset-amd64: diff --git a/Dockerfile b/Dockerfile index e7b5b679c..ca493aa3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,31 @@ +# syntax=docker/dockerfile:1.10 + FROM node:24.18.0-alpine AS base FROM base AS builder -RUN apk add --no-cache libc6-compat -RUN apk update - -# Set working directory WORKDIR /app -RUN apk add --no-cache libc6-compat curl bash -RUN apk update +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 + COPY . . +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + pnpm install --recursive --offline --frozen-lockfile -RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile - -# Copy static data as it is not part of the build -COPY static-data ./static-data ARG SKIP_ENV_VALIDATION='true' ARG CI='true' ARG DISABLE_REDIS_LOGS='true' +ARG TARGETPLATFORM -RUN corepack enable pnpm && pnpm build +RUN --mount=type=secret,id=TURBO_API,env=TURBO_API \ + --mount=type=secret,id=TURBO_TEAM,env=TURBO_TEAM \ + --mount=type=secret,id=TURBO_TOKEN,env=TURBO_TOKEN \ + --mount=type=secret,id=TURBO_REMOTE_CACHE_SIGNATURE_KEY,env=TURBO_REMOTE_CACHE_SIGNATURE_KEY \ + TURBO_PLATFORM="${TARGETPLATFORM:-linux/amd64}" pnpm build FROM base AS runner WORKDIR /app diff --git a/package.json b/package.json index be1710f32..3bf89d731 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "build": "cross-env CI=true turbo build", "clean": "git clean -xdf node_modules", "clean:workspaces": "turbo clean", + "ci:act:all": "bash scripts/ci/act.sh all", + "ci:act:cleanup": "bash scripts/ci/act.sh cleanup", + "ci:act:docker": "bash scripts/ci/act.sh docker", + "ci:act:fast": "bash scripts/ci/act.sh fast", "cli": "pnpm with-env tsx packages/cli/index.ts", "db:migration:mysql:generate": "pnpm -F db migration:mysql:generate", "db:migration:mysql:run": "pnpm -F db migration:mysql:run", @@ -33,6 +37,9 @@ "typecheck": "turbo typecheck", "with-env": "dotenv -e .env --" }, + "dependencies": { + "jsonpath-plus": "catalog:" + }, "devDependencies": { "@playwright/test": "catalog:", "@semantic-release/changelog": "catalog:", @@ -64,8 +71,5 @@ "node": ">=24.18.0", "pnpm": ">=11.10.0" }, - "packageManager": "pnpm@11.10.0", - "dependencies": { - "jsonpath-plus": "catalog:" - } + "packageManager": "pnpm@11.10.0" } diff --git a/scripts/ci/act.sh b/scripts/ci/act.sh new file mode 100644 index 000000000..767563db4 --- /dev/null +++ b/scripts/ci/act.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +set -euo pipefail + +mode="${1:-all}" +case "$mode" in + fast) job="fast-gate" ;; + docker) job="container" ;; + all) job="" ;; + cleanup) job="cleanup" ;; + *) + echo "Usage: $0 {fast|docker|all|cleanup}" >&2 + exit 2 + ;; +esac + +command -v act >/dev/null 2>&1 || { + echo "act is required: https://nektosact.com/installation/index.html" >&2 + exit 1 +} + +docker info >/dev/null 2>&1 || { + echo "Docker is not running" >&2 + exit 1 +} + +if [[ -z "${DOCKER_HOST:-}" ]]; then + DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true) + export DOCKER_HOST +fi + +head_sha=$(git rev-parse HEAD) +base_sha=$(git rev-parse HEAD~1 2>/dev/null || echo "$head_sha") +head_ref=$(git branch --show-current) +head_ref=${head_ref:-act-local} +event_file=$(mktemp "${TMPDIR:-/tmp}/homarr-act-event.XXXXXX") +trap 'rm -f "$event_file"' EXIT + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g'; } + +if [[ "$mode" == "cleanup" ]]; then + event_name="workflow_dispatch" + cat >"$event_file" <"$event_file" <