From 2ea453ea2f26b884eff73a24574d5f502caa7b57 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Thu, 16 Oct 2025 22:07:56 +0200 Subject: [PATCH] feat: add build --- .github/release-drafter-config.yml | 48 +++++++++++++ .github/workflows/auto-labeler.yaml | 21 ++++++ .github/workflows/job-build.yaml | 45 ++++++++++++ .github/workflows/job-draft-release.yaml | 18 +++++ .github/workflows/pipeline-default.yaml | 79 ++++++++++++++++++++++ package.json | 2 +- pnpm-workspace.yaml | 3 + src/api/api.ts | 3 +- src/api/endpoints/endpoints.manage.ts | 5 +- src/api/endpoints/endpoints.message.ts | 5 +- src/api/plugins/plugins.auth.ts | 3 +- src/auth/auth.admin.ts | 5 +- src/auth/auth.oidc.ts | 2 +- src/config/config.ts | 2 +- src/server/server.ts | 31 ++++++--- src/services/sessions/sessions.provider.ts | 3 +- tests/mqtt.test.ts | 1 + tests/utils/utils.statements.ts | 2 +- tests/utils/utils.world.ts | 12 +++- 19 files changed, 267 insertions(+), 23 deletions(-) create mode 100644 .github/release-drafter-config.yml create mode 100644 .github/workflows/auto-labeler.yaml create mode 100644 .github/workflows/job-build.yaml create mode 100644 .github/workflows/job-draft-release.yaml create mode 100644 .github/workflows/pipeline-default.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml new file mode 100644 index 0000000..f72741a --- /dev/null +++ b/.github/release-drafter-config.yml @@ -0,0 +1,48 @@ +name-template: '$RESOLVED_VERSION 🌈' +tag-template: '$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +autolabeler: + - label: 'chore' + files: + - '*.md' + branch: + - '/docs{0,1}\/.+/' + - label: 'bug' + branch: + - '/fix\/.+/' + title: + - '/fix/i' + - label: 'enhancement' + branch: + - '/feature\/.+/' + - '/feat\/.+/' + title: + - '/feat:.+/' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/auto-labeler.yaml b/.github/workflows/auto-labeler.yaml new file mode 100644 index 0000000..f134cf8 --- /dev/null +++ b/.github/workflows/auto-labeler.yaml @@ -0,0 +1,21 @@ +name: Auto Labeler +on: + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + auto-labeler: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter-config.yml + disable-releaser: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/job-build.yaml b/.github/workflows/job-build.yaml new file mode 100644 index 0000000..27badb0 --- /dev/null +++ b/.github/workflows/job-build.yaml @@ -0,0 +1,45 @@ +name: Build +on: + workflow_call: + +env: + DO_NOT_TRACK: '1' + NODE_VERSION: '23.x' + PNPM_VERSION: 10.18.0 + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '${{ env.NODE_VERSION }}' + registry-url: '${{ env.NODE_REGISTRY }}' + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Run tests + run: pnpm test diff --git a/.github/workflows/job-draft-release.yaml b/.github/workflows/job-draft-release.yaml new file mode 100644 index 0000000..852935d --- /dev/null +++ b/.github/workflows/job-draft-release.yaml @@ -0,0 +1,18 @@ +name: Draft release +on: + workflow_call: +jobs: + draft-release: + name: Update release drafter + permissions: + contents: write + pull-requests: write + environment: release + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter-config.yml + publish: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pipeline-default.yaml b/.github/workflows/pipeline-default.yaml new file mode 100644 index 0000000..d6d514a --- /dev/null +++ b/.github/workflows/pipeline-default.yaml @@ -0,0 +1,79 @@ +name: Build and release + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +env: + DO_NOT_TRACK: '1' + DOCKER_REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +permissions: + contents: write + packages: read + pull-requests: write + id-token: write + actions: read + security-events: write +jobs: + build: + uses: ./.github/workflows/job-build.yaml + name: Build + + update-release-draft: + needs: build + if: github.ref == 'refs/heads/main' + uses: ./.github/workflows/job-draft-release.yaml + + release: + permissions: + contents: read + packages: write + attestations: write + id-token: write + pages: write + name: Release + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: update-release-draft + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN}} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/package.json b/package.json index 2bd9037..d155511 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test:unit": "vitest --run --passWithNoTests", "test": "pnpm run \"/^test:/\"" }, - "packageManager": "pnpm@10.6.0", + "packageManager": "pnpm@10.18.0", "files": [ "dist" ], diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..f2721cf --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - better-sqlite3 + - esbuild diff --git a/src/api/api.ts b/src/api/api.ts index 5e25d7b..adde5bc 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,8 +1,9 @@ import { type FastifyPluginAsync } from 'fastify'; +import { z } from 'zod'; + import { manageEndpoints } from './endpoints/endpoints.manage.ts'; import { authPlugin } from './plugins/plugins.auth.ts'; import { messageEndpoints } from './endpoints/endpoints.message.ts'; -import { z } from 'zod'; const api: FastifyPluginAsync = async (fastify) => { fastify.route({ diff --git a/src/api/endpoints/endpoints.manage.ts b/src/api/endpoints/endpoints.manage.ts index 9b60a49..665d3f1 100644 --- a/src/api/endpoints/endpoints.manage.ts +++ b/src/api/endpoints/endpoints.manage.ts @@ -1,8 +1,9 @@ +import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; +import { z } from 'zod'; + import { JwtAuth } from '#root/auth/auth.jwt.ts'; import { statementSchema } from '#root/auth/auth.schemas.ts'; import { Config } from '#root/config/config.ts'; -import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; -import { z } from 'zod'; const manageEndpoints: FastifyPluginAsyncZod = async (fastify) => { const config = fastify.services.get(Config); diff --git a/src/api/endpoints/endpoints.message.ts b/src/api/endpoints/endpoints.message.ts index 2d8915a..bc012c1 100644 --- a/src/api/endpoints/endpoints.message.ts +++ b/src/api/endpoints/endpoints.message.ts @@ -1,8 +1,9 @@ -import { Config } from '#root/config/config.ts'; -import { MqttServer } from '#root/server/server.ts'; import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; import { z } from 'zod'; +import { Config } from '#root/config/config.ts'; +import { MqttServer } from '#root/server/server.ts'; + const messageEndpoints: FastifyPluginAsyncZod = async (fastify) => { const config = fastify.services.get(Config); diff --git a/src/api/plugins/plugins.auth.ts b/src/api/plugins/plugins.auth.ts index aacd613..3647073 100644 --- a/src/api/plugins/plugins.auth.ts +++ b/src/api/plugins/plugins.auth.ts @@ -1,6 +1,7 @@ -import { SessionProvider } from '#root/services/sessions/sessions.provider.ts'; import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; +import { SessionProvider } from '#root/services/sessions/sessions.provider.ts'; + const authPlugin: FastifyPluginAsyncZod = async (fastify) => { fastify.addHook('onRequest', async (req, reply) => { const authProvider = req.headers['x-auth-provider']; diff --git a/src/auth/auth.admin.ts b/src/auth/auth.admin.ts index be99fb0..15a5b4a 100644 --- a/src/auth/auth.admin.ts +++ b/src/auth/auth.admin.ts @@ -1,8 +1,9 @@ -import type { Services } from '#root/utils/services.ts'; -import { Config } from '#root/config/config.ts'; import type { AuthProvider } from './auth.provider.ts'; import { ADMIN_STATEMENTS } from './auth.consts.ts'; +import type { Services } from '#root/utils/services.ts'; +import { Config } from '#root/config/config.ts'; + class AdminAuth implements AuthProvider { #services: Services; diff --git a/src/auth/auth.oidc.ts b/src/auth/auth.oidc.ts index 92e5650..dd2868e 100644 --- a/src/auth/auth.oidc.ts +++ b/src/auth/auth.oidc.ts @@ -2,10 +2,10 @@ import jwt from 'jsonwebtoken'; import type { Statement } from './auth.schemas.ts'; import type { AuthProvider } from './auth.provider.ts'; +import { ADMIN_STATEMENTS, READER_STATEMENTS, WRITER_STATEMENTS } from './auth.consts.ts'; import type { Services } from '#root/utils/services.ts'; import { Config } from '#root/config/config.ts'; -import { ADMIN_STATEMENTS, READER_STATEMENTS, WRITER_STATEMENTS } from './auth.consts.ts'; class OidcAuth implements AuthProvider { #services: Services; diff --git a/src/config/config.ts b/src/config/config.ts index f0ae998..1bf6035 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -59,7 +59,7 @@ class Config { } public get tcp() { - const enabled = (process.env.TCP_ENABLED = 'true'); + const enabled = process.env.TCP_ENABLED === 'true'; const port = process.env.TCP_PORT ? parseInt(process.env.TCP_PORT) : 1883; return { enabled, diff --git a/src/server/server.ts b/src/server/server.ts index d994990..e88d01e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -2,13 +2,7 @@ import tcp from 'node:net'; import type { IncomingMessage } from 'node:http'; import swagger from '@fastify/swagger'; -import type { ZodTypeProvider } from 'fastify-type-provider-zod'; -import { - jsonSchemaTransform, - createJsonSchemaTransform, - serializerCompiler, - validatorCompiler, -} from 'fastify-type-provider-zod'; +import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'; import scalar from '@scalar/fastify-api-reference'; import { type AuthenticateHandler, @@ -21,14 +15,14 @@ import aedes from 'aedes'; import fastify, { type FastifyInstance } from 'fastify'; import fastifyWebSocket from '@fastify/websocket'; import { createWebSocketStream } from 'ws'; +import fastifySensible from '@fastify/sensible'; import { api } from '../api/api.ts'; import { TopicsHandler } from '#root/topics/topics.handler.ts'; -import type { Services } from '#root/utils/services.ts'; +import { destroy, type Services } from '#root/utils/services.ts'; import { Session } from '#root/services/sessions/sessions.session.ts'; import { SessionProvider } from '#root/services/sessions/sessions.provider.ts'; -import fastifySensible from '@fastify/sensible'; import { Config } from '#root/config/config.ts'; type Aedes = ReturnType; @@ -188,6 +182,25 @@ class MqttServer { } return this.#tcp; }; + + [destroy] = async () => { + if (this.#http) { + const http = await this.#http; + await http.close(); + } + await new Promise((resolve, reject) => { + if (this.#tcp) { + this.#tcp.close((err) => { + if (err) { + return reject(err); + } + resolve(); + }); + } else { + resolve(); + } + }); + }; } export { MqttServer }; diff --git a/src/services/sessions/sessions.provider.ts b/src/services/sessions/sessions.provider.ts index c89c39f..012d433 100644 --- a/src/services/sessions/sessions.provider.ts +++ b/src/services/sessions/sessions.provider.ts @@ -1,6 +1,7 @@ -import type { AuthProvider } from '#root/auth/auth.provider.ts'; import { Session } from './sessions.session.ts'; +import type { AuthProvider } from '#root/auth/auth.provider.ts'; + class SessionProvider { #handlers: Map; diff --git a/tests/mqtt.test.ts b/tests/mqtt.test.ts index 677785f..60a1f64 100644 --- a/tests/mqtt.test.ts +++ b/tests/mqtt.test.ts @@ -39,6 +39,7 @@ describe('mqtt', () => { it('should not be able to publish if not allowed', async () => { const [client] = await world.connect([]); + // eslint-disable-next-line const promise = client.publishAsync('test', 'test'); // TODO: why does this not throw? diff --git a/tests/utils/utils.statements.ts b/tests/utils/utils.statements.ts index 5ba9a2e..5d3f6f4 100644 --- a/tests/utils/utils.statements.ts +++ b/tests/utils/utils.statements.ts @@ -1,4 +1,4 @@ -import type { Statement } from '#root/access/access.schemas.ts'; +import type { Statement } from '#root/auth/auth.schemas.ts'; const statements = { all: [ diff --git a/tests/utils/utils.world.ts b/tests/utils/utils.world.ts index dda922c..1853c9a 100644 --- a/tests/utils/utils.world.ts +++ b/tests/utils/utils.world.ts @@ -33,6 +33,16 @@ const createWorld = async (options: WorldOptions) => { backbone.services.set(Config, { jwtSecret: 'test', adminToken: 'test', + api: { + enabled: true, + }, + ws: { + enabled: true, + }, + tcp: { + enabled: false, + port: 1883, + }, }); const accessTokens = backbone.services.get(JwtAuth); backbone.sessionProvider.register('token', accessTokens); @@ -61,7 +71,7 @@ const createWorld = async (options: WorldOptions) => { }, destroy: async () => { await Promise.all(sockets.map((s) => s.endAsync())); - await fastify.close(); + await backbone.destroy(); }, }; };