diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d717c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +.env +.gitignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d987777 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Copy this file to .env and fill in the values + +# Url to send the filtered webhooks to (required) +WEBHOOK_URL= + +# Set to true to drop events not defined in filterConfiguration.json +BLOCK_UNDEFINED_EVENTS=false + diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..4bd3d51 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,67 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + release: + types: [published] + +permissions: + contents: read + +jobs: + check: + name: Build and type check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 26 + cache: npm + + - run: npm ci + - run: npm run build + + container: + name: Build image + needs: check + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + if: github.event_name == 'release' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + # Stable releases also get the latest and major.minor tags, + # prereleases get only their exact version tag. + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + platforms: linux/amd64 + push: ${{ github.event_name == 'release' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index f06235c..9c97bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c996569 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Build stage: compile the TypeScript sources +FROM node:26.12.1-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# Drop dev dependencies so node_modules can be reused as-is in the runtime stage +RUN npm prune --omit=dev + +# Runtime stage: only production dependencies and compiled output +FROM node:26.12.1-alpine + +WORKDIR /app + +ENV NODE_ENV=production +# Listen on all interfaces so the server is reachable from outside the container +ENV HOST=0.0.0.0 +ENV PORT=3000 + +COPY --from=build --chown=node:node /app/package*.json ./ +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist + + +# Run as the unprivileged user provided by the node image +USER node + +EXPOSE 3000 + +# WEBHOOK_URL must be provided at runtime, e.g. `docker run -e WEBHOOK_URL=...` +CMD ["node", "dist/src/server.js"] diff --git a/README.md b/README.md index f9ce996..d92a701 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ And you are done ! Now you can edit the *fully typed* file [filterConfiguration.json](src/filterConfiguration.json), to filter as you would like your webhooks. +### With Docker Compose + +- Copy [.env.example](.env.example) to `.env` and set `WEBHOOK_URL` +- Run `docker compose up -d` [^1] + +The port on the host can be configured using [the compose file](compose.yml). The [filterConfiguration.json](src/filterConfiguration.json) is mounted into the container, to apply changes, restart the container with `docker compose restart`[^2]. + ## Configuration ### Environment variables @@ -42,3 +49,6 @@ If you want to test locally, you will need to redirect the webhook sent by Githu You can also have access to the recent deliveries of a webhook, as explained here: https://docs.github.com/en/developers/webhooks-and-events/webhooks/testing-webhooks. To generate new events json schema, add them to the file `events/index.ts` and run the command `npm run generate-schemas`. + +[^1]: Or with `podman compose up -d` +[^2]: Or with `podman compose restart` \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..173bb98 --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +services: + github-webhook-filter: + image: ghcr.io/rezoleo/github-webhook-filter:1.0.0 + container_name: github-webhook-filter + init: true + ports: + - 3000:3000 + environment: + # URL to send the events to + - WEBHOOK_URL=${WEBHOOK_URL:?WEBHOOK_URL is required} + # Choose wether to block events other than those defined in filterConfiguration + - BLOCK_UNDEFINED_EVENTS=${BLOCK_UNDEFINED_EVENTS:-false} + volumes: + # Edit the filters without rebuilding the image, restart the container to apply them + - ./path/to/filterConfiguration.json:/app/dist/src/filterConfiguration.json:ro + restart: unless-stopped diff --git a/package.json b/package.json index dd17a36..a8f0e9e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "prebuild": "rm -rf dist", - "build": "tsc -p tsconfig.json & npm run copyfiles", + "build": "tsc -p tsconfig.json && npm run copyfiles", "copyfiles": "copyfiles package* dist", "start": "node dist/src/server.js", "watch": "ts-node-dev src/server.ts", diff --git a/src/routes.ts b/src/routes.ts index c837e8f..0f1eb7c 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,14 +1,12 @@ import { FastifyInstance } from 'fastify'; import { EventBody, handleEvents } from './handleEvents'; - - export default async function routes(fastify: FastifyInstance) { fastify.post<{Body: EventBody}>('*', (request, reply) => { const { headers, body } = request; const event = headers['x-github-event'] as string; if (!event) { - reply.send(400) + reply.code(400).send() return } const {status, message} = handleEvents(event, body, fastify.log); diff --git a/src/server.ts b/src/server.ts index e2f51e5..6d1633d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,3 +13,12 @@ server.listen({host: process.env.HOST || "127.0.0.1", port: Number(process.env.P } server.log.info(`Server listening at ${address}`); }); + +async function closeGracefully(signal: NodeJS.Signals) { + server.log.info(`Received signal to terminate: ${signal}`); + + await server.close(); + process.kill(process.pid, signal); +} +process.once('SIGINT', closeGracefully); +process.once('SIGTERM', closeGracefully); diff --git a/tsconfig.json b/tsconfig.json index 6449545..f5b3144 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */