Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 26
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
Expand Down
49 changes: 0 additions & 49 deletions .github/workflows/deploy.yml

This file was deleted.

66 changes: 0 additions & 66 deletions .github/workflows/docker.yml

This file was deleted.

96 changes: 96 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 (/multimeter/) → GitHub Pages BASE_PATH=/ → the container image
# The image copies that prebuilt apps/web/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@v6
- uses: pnpm/action-setup@v6 # reads packageManager from package.json
- uses: actions/setup-node@v6
with:
node-version: 26
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build for GitHub Pages
run: pnpm --filter web build && mv apps/web/dist pages-dist
- name: Build for the container image
run: BASE_PATH=/ pnpm --filter web 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/multimeter (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=apps/web/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=apps/web/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
16 changes: 12 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
# docker build -t multimeter . # served at /
# docker build --build-arg BASE_PATH=/multimeter/ -t multimeter . # served at /multimeter/

FROM node:22-alpine AS build
FROM node:26-alpine AS build
WORKDIR /app
RUN corepack enable
RUN npm install -g corepack && corepack enable
COPY . .
RUN pnpm install --frozen-lockfile
ARG BASE_PATH=/
RUN BASE_PATH="$BASE_PATH" pnpm --filter web 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/apps/web/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=apps/web/dist .
FROM serve AS prebuilt
COPY --from=dist . /usr/share/nginx/html${BASE_PATH}

# Default: build from source.
FROM serve
COPY --from=build /app/apps/web/dist /usr/share/nginx/html${BASE_PATH}
3 changes: 3 additions & 0 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export default defineConfig(({ mode }) => {
server: { host: true },
test: {
environment: 'jsdom',
// Node 25+ ships its own global localStorage (unusable without --localstorage-file), which
// shadows jsdom's. Turn it off so tests get the jsdom one.
execArgv: ['--no-experimental-webstorage'],
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
Expand Down
Loading