From 9c765577e903f8d3fd6a85566f844c8e44fb8521 Mon Sep 17 00:00:00 2001 From: mannes Date: Thu, 24 Sep 2026 08:37:38 +0200 Subject: [PATCH 1/3] Build on Node 26 Node 26 becomes Active LTS in October; move the Docker build stage and the GitHub Actions workflows from Node 22 to 26. - @types/node ^26. --- .github/workflows/deploy.yml | 2 +- Dockerfile | 2 +- package-lock.json | 16 ++++++++-------- package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b404bd2..72ccaac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 26 cache: npm - run: npm ci - run: npm run build # tsc -b && vite build -> dist/ (base '/brushlog/') diff --git a/Dockerfile b/Dockerfile index dfb8cca..37a02b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # docker build -t brushlog . # served at / # docker build --build-arg BASE_PATH=/brushlog/ -t brushlog . # served at /brushlog/ -FROM node:22-alpine AS build +FROM node:26-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci diff --git a/package-lock.json b/package-lock.json index 481aa98..08f3ba7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", - "@types/node": "^24.13.6", + "@types/node": "^26.6.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/web-bluetooth": "^0.0.21", @@ -2947,13 +2947,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.13.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", - "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/react": { @@ -6234,9 +6234,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 52fc49d..8c971fc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", - "@types/node": "^24.13.6", + "@types/node": "^26.6.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/web-bluetooth": "^0.0.21", From 133f3385cce5a9d9cebd8873e437abb2fd755c17 Mon Sep 17 00:00:00 2001 From: mannes Date: Thu, 24 Sep 2026 08:40:21 +0200 Subject: [PATCH 2/3] Release on v* tags: Pages deploy and container image Merging to main no longer publishes anything. Pushing a v* tag deploys to GitHub Pages and pushes ghcr.io/libreble/brushlog as , . and latest. Pull requests still build and smoke-test the image; the Pages workflow can still be run by hand from main. --- .github/workflows/deploy.yml | 4 +++- .github/workflows/docker.yml | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 72ccaac..d8e972b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,10 @@ name: Deploy to GitHub Pages +# Releases only: push a v* tag to publish. Merging to main doesn't deploy. +# (workflow_dispatch on main redeploys main by hand, e.g. after a Pages outage.) on: push: - branches: [main] + tags: ['v*'] workflow_dispatch: # Least privilege needed to publish to Pages. diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 035e978..5486395 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,13 +1,12 @@ name: Container image # Publishes ghcr.io/libreble/ (linux/amd64 + linux/arm64) with the built-in -# GITHUB_TOKEN — no registry account or secrets. Pull requests build and smoke-test only. +# GITHUB_TOKEN — no registry account or secrets. Releases only: a v* tag pushes +# , . and latest. Pull requests build and smoke-test only. on: push: - branches: [main] tags: ['v*'] pull_request: - workflow_dispatch: permissions: contents: read @@ -30,8 +29,6 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} From 6287d097c1a2edc5654dffbbf108ab19f56b0a7b Mon Sep 17 00:00:00 2001 From: mannes Date: Thu, 24 Sep 2026 08:44:01 +0200 Subject: [PATCH 3/3] One release workflow: install and build once for Pages and the image deploy.yml and docker.yml become release.yml. A single job installs once and builds on the runner, uploads the Pages artifact and builds the image from the prebuilt output (new Dockerfile target `prebuilt`, fed via --build-context dist=...). The arm64 image no longer runs an emulated Node build. Building the Dockerfile from source (the default target) is unchanged for self-hosters. --- .github/workflows/deploy.yml | 45 ----------------- .github/workflows/docker.yml | 63 ----------------------- .github/workflows/release.yml | 95 +++++++++++++++++++++++++++++++++++ Dockerfile | 12 ++++- 4 files changed, 105 insertions(+), 110 deletions(-) delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index d8e972b..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Deploy to GitHub Pages - -# Releases only: push a v* tag to publish. Merging to main doesn't deploy. -# (workflow_dispatch on main redeploys main by hand, e.g. after a Pages outage.) -on: - push: - tags: ['v*'] - workflow_dispatch: - -# Least privilege needed to publish to Pages. -permissions: - contents: read - pages: write - id-token: write - -# One deploy at a time; let a newer push cancel an in-flight run. -concurrency: - group: pages - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 - with: - node-version: 26 - cache: npm - - run: npm ci - - run: npm run build # tsc -b && vite build -> dist/ (base '/brushlog/') - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 - with: - path: dist - - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 5486395..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Container image - -# Publishes ghcr.io/libreble/ (linux/amd64 + linux/arm64) with the built-in -# GITHUB_TOKEN — no registry account or secrets. Releases only: a v* tag pushes -# , . and latest. Pull requests build and smoke-test only. -on: - push: - tags: ['v*'] - pull_request: - -permissions: - contents: read - packages: write - -concurrency: - group: docker-${{ github.ref }} - cancel-in-progress: true - -jobs: - image: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - # Build for the runner, run it, and check the page, assets, SW, manifest and SPA fallback. - - uses: docker/build-push-action@v6 - with: - context: . - load: true - tags: smoke:test - cache-from: type=gha - cache-to: type=gha,mode=max - - run: docker run -d --name smoke -p 8080:8080 smoke:test - - run: ./docker/smoke.sh http://localhost:8080/ - - if: failure() - run: docker logs smoke - - - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - if: github.event_name != 'pull_request' - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - annotations: ${{ steps.meta.outputs.annotations }} - cache-from: type=gha diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..01979aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +# Releases only: push a v* tag. Merging to main publishes nothing. +# +# One job installs once and builds on the runner — twice, because Vite bakes the base path in: +# default base (/brushlog/) → GitHub Pages BASE_PATH=/ → the container image +# The image copies that prebuilt dist/ into nginx (Dockerfile target `prebuilt`), so the +# arm64 variant needs no emulated Node build. Pull requests run the same build and +# smoke-test the image without publishing; workflow_dispatch on main redeploys Pages. +on: + push: + tags: ['v*'] + pull_request: + workflow_dispatch: + +permissions: + contents: read + packages: write + pages: write + id-token: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 26 + cache: npm + - run: npm ci + - name: Build for GitHub Pages + run: npm run build && mv dist pages-dist + - name: Build for the container image + run: BASE_PATH=/ npm run build + + - if: github.event_name != 'pull_request' + uses: actions/configure-pages@v6 + - if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v5 + with: + path: pages-dist + + # ghcr.io/libreble/brushlog (linux/amd64 + linux/arm64) via the built-in GITHUB_TOKEN. + - uses: docker/setup-buildx-action@v3 + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - uses: docker/build-push-action@v6 + with: + context: . + target: prebuilt + build-contexts: dist=dist + load: true + tags: smoke:test + - run: docker run -d --name smoke -p 8080:8080 smoke:test + - run: ./docker/smoke.sh http://localhost:8080/ + - if: failure() + run: docker logs smoke + - if: startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - if: startsWith(github.ref, 'refs/tags/v') + uses: docker/build-push-action@v6 + with: + context: . + target: prebuilt + build-contexts: dist=dist + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/Dockerfile b/Dockerfile index 37a02b5..c281c56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,10 +11,18 @@ COPY . . ARG BASE_PATH=/ RUN BASE_PATH="$BASE_PATH" npm run build -FROM nginxinc/nginx-unprivileged:1.29-alpine +FROM nginxinc/nginx-unprivileged:1.29-alpine AS serve ARG BASE_PATH=/ ENV BASE_PATH=$BASE_PATH COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template -COPY --from=build /app/dist /usr/share/nginx/html${BASE_PATH} EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO /dev/null http://127.0.0.1:8080/healthz || exit 1 + +# Release CI: serve a dist/ already built on the runner (.github/workflows/release.yml): +# docker buildx build --target prebuilt --build-context dist=dist . +FROM serve AS prebuilt +COPY --from=dist . /usr/share/nginx/html${BASE_PATH} + +# Default: build from source. +FROM serve +COPY --from=build /app/dist /usr/share/nginx/html${BASE_PATH}