From 6645fc915048dcb0f9485f240da2bcf525d089d5 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 1 Nov 2026 02:10:50 -0500 Subject: [PATCH] Gateway container, compose file for server-gib, and Gitea CI with deploy --- .gitea/workflows/gateway.yml | 94 ++++++++++++++++++++++++++++++++++++ deploy/compose.yml | 27 +++++++++++ gateway/.containerignore | 3 ++ gateway/.dockerignore | 1 + gateway/Containerfile | 26 ++++++++++ 5 files changed, 151 insertions(+) create mode 100644 .gitea/workflows/gateway.yml create mode 100644 deploy/compose.yml create mode 100644 gateway/.containerignore create mode 120000 gateway/.dockerignore create mode 100644 gateway/Containerfile diff --git a/.gitea/workflows/gateway.yml b/.gitea/workflows/gateway.yml new file mode 100644 index 0000000..ef49c38 --- /dev/null +++ b/.gitea/workflows/gateway.yml @@ -0,0 +1,94 @@ +name: Gateway + +# Every push touching the gateway: typecheck and tests. On main, also build the image, push it to +# the Gitea registry tagged with the commit and `latest`, and ask server-gib to deploy it now. +# Watchtower's nightly pull there is the fallback when the deploy trigger fails. +# +# Uses the user-level secrets REGISTRY_USER and REGISTRY_PASSWORD, and the repo secret +# DEPLOY_SSH_KEY_HOME, a key that server-gib only accepts for ~/Server/ci-deploy.sh. + +on: + push: + paths: + - 'gateway/**' + - '.gitea/workflows/gateway.yml' + pull_request: + paths: + - 'gateway/**' + +concurrency: + group: gateway-${{ gitea.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: build + steps: + - name: Checkout + # Plain git instead of actions/checkout: rootless podman on some runners rejects symlinks + # inside cached actions. + env: + GITEA_TOKEN: ${{ gitea.token }} + run: | + git init -q . + git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git" + auth="$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 -w0)" + git -c http.extraHeader="Authorization: basic $auth" \ + fetch -q --depth 1 origin "${{ gitea.sha }}" + git checkout -q FETCH_HEAD + + - name: Set up Bun + # The install script instead of oven-sh/setup-bun, for the same symlink reason. + run: | + curl -fsSL https://bun.sh/install | bash -s "bun-v1.4.0" >/dev/null + echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + + - name: Typecheck and test + working-directory: gateway + run: | + bun install --frozen-lockfile + bun run typecheck + bun test + + image: + needs: [check] + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + runs-on: build + env: + IMAGE: git.gbrown.org/gib/wristcode-gateway + steps: + - name: Checkout + env: + GITEA_TOKEN: ${{ gitea.token }} + run: | + git init -q . + git remote add origin "${{ gitea.server_url }}/${{ gitea.repository }}.git" + auth="$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 -w0)" + git -c http.extraHeader="Authorization: basic $auth" \ + fetch -q --depth 1 origin "${{ gitea.sha }}" + git checkout -q FETCH_HEAD + + - name: Build + run: docker build -f gateway/Containerfile -t "$IMAGE:${{ gitea.sha }}" -t "$IMAGE:latest" gateway + + - name: Push + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin + docker push "$IMAGE:${{ gitea.sha }}" + docker push "$IMAGE:latest" + + - name: Deploy + # The key comes in through env, not ${{ secrets.* }} in the script: the runner prints the + # script, and Gitea does not mask a multi-line secret split across lines. + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY_HOME }} + run: | + install -m600 /dev/null "$HOME/.deploy_key" + printf '%s\n' "$DEPLOY_SSH_KEY" > "$HOME/.deploy_key" + ssh -i "$HOME/.deploy_key" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=accept-new \ + -o ConnectTimeout=15 \ + gib@192.168.1.2 wristcode \ + || echo "::warning::deploy trigger failed; Watchtower will deploy at 04:00 instead" + rm -f "$HOME/.deploy_key" diff --git a/deploy/compose.yml b/deploy/compose.yml new file mode 100644 index 0000000..aff0f5a --- /dev/null +++ b/deploy/compose.yml @@ -0,0 +1,27 @@ +# The gateway on server-gib, at ~/Server/Wristcode/compose.yml. Nginx Proxy Manager serves +# wristcode.gbrown.org and forwards to wristcode-gateway:8080 over the nginx-bridge network. +# CI deploys new images through ~/Server/ci-deploy.sh. Watchtower's nightly pull is the fallback. +# +# .env beside this file (mode 600) holds the APNs settings: +# APNS_KEY_ID=... the key's ID from the Apple Developer portal +# APNS_TEAM_ID=GQJNM5YK2M +# APNS_KEY_PATH=/data/apns.p8 the .p8 file, placed in ./data +name: wristcode + +networks: + nginx-bridge: + external: true + +services: + wristcode-gateway: + image: git.gbrown.org/gib/wristcode-gateway:latest + container_name: wristcode-gateway + hostname: wristcode-gateway + networks: ['nginx-bridge'] + env_file: [.env] + environment: + - TZ=America/New_York + volumes: + - ./data:/data + labels: ['com.centurylinklabs.watchtower.enable=true'] + restart: unless-stopped diff --git a/gateway/.containerignore b/gateway/.containerignore new file mode 100644 index 0000000..95cdeca --- /dev/null +++ b/gateway/.containerignore @@ -0,0 +1,3 @@ +node_modules +data +test diff --git a/gateway/.dockerignore b/gateway/.dockerignore new file mode 120000 index 0000000..092a75d --- /dev/null +++ b/gateway/.dockerignore @@ -0,0 +1 @@ +.containerignore \ No newline at end of file diff --git a/gateway/Containerfile b/gateway/Containerfile new file mode 100644 index 0000000..a472410 --- /dev/null +++ b/gateway/Containerfile @@ -0,0 +1,26 @@ +# The WristCode gateway. The gateway has no runtime dependencies, so the image is the Bun base plus +# src/. State lives in /data (one SQLite file), which compose mounts from the host. +# Builds the same under docker and podman: `docker build -f gateway/Containerfile gateway`. + +# Pinned by digest. Bump the tag and digest together. +FROM docker.io/oven/bun:1.4.0-alpine@sha256:07235578f79ef8c6f97d94aee7938e76f5cdba5f21ae5dbfdd3d3d38058437eb + +WORKDIR /app +COPY package.json tsconfig.json ./ +COPY src ./src + +# `wristcode ` for operators: docker exec -it wristcode-gateway wristcode pair +RUN printf '#!/bin/sh\nexec bun /app/src/cli.ts "$@"\n' > /usr/local/bin/wristcode \ + && chmod +x /usr/local/bin/wristcode \ + && mkdir -p /data && chown bun:bun /data + +ENV NODE_ENV=production \ + PORT=8080 \ + WRISTCODE_DB=/data/wristcode.sqlite + +USER bun +EXPOSE 8080 +VOLUME /data +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/v1/health || exit 1 +CMD ["bun", "src/main.ts"]