Deploy
This commit is contained in:
parent
379ee14739
commit
0ed480777e
9 changed files with 214 additions and 68 deletions
14
.dockerignore
Normal file
14
.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# The server image only needs `shared/` and `backend/`.
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/lib
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.forgejo
|
||||||
|
.devcontainer
|
||||||
|
.vscode
|
||||||
|
frontend
|
||||||
|
media
|
||||||
|
*.log
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
110
.forgejo/workflows/deploy.yml
Normal file
110
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
name: Build & deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ gitea.workflow }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ---- Static website -> /pages mount on the runner host --------------------
|
||||||
|
website:
|
||||||
|
name: Build & deploy website
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
# No lockfiles are committed (see .gitignore), so use `npm install`.
|
||||||
|
run: npm install && npm run init
|
||||||
|
|
||||||
|
- name: Build (shared -> frontend -> backend)
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy to host pages mount
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
apt-get update && apt-get install -y rsync
|
||||||
|
rsync -a --delete frontend/dist/ /pages/declared
|
||||||
|
|
||||||
|
# ---- Server Docker image -> Forgejo container registry --------------------
|
||||||
|
server-image:
|
||||||
|
name: Build & publish server image
|
||||||
|
runs-on: docker
|
||||||
|
# No registry push on PRs; build validation still happens in the website job.
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Docker CLI
|
||||||
|
run: |
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-27.5.1.tgz" \
|
||||||
|
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: |
|
||||||
|
network=host
|
||||||
|
|
||||||
|
- name: Resolve registry vars
|
||||||
|
id: registry
|
||||||
|
env:
|
||||||
|
CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }}
|
||||||
|
run: |
|
||||||
|
host="${CONTAINER_REGISTRY_HOST:-${{ gitea.server_url }}}"
|
||||||
|
host="${host#https://}"
|
||||||
|
host="${host#http://}"
|
||||||
|
host="${host%/}"
|
||||||
|
if [ "$host" = "forgejo:3000" ]; then
|
||||||
|
host="127.0.0.1:13000"
|
||||||
|
fi
|
||||||
|
repo=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
owner="${repo%%/*}"
|
||||||
|
{
|
||||||
|
echo "host=${host}"
|
||||||
|
echo "owner=${owner}"
|
||||||
|
echo "image=${host}/${repo}-server"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to Forgejo Container Registry
|
||||||
|
uses: https://github.com/docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ steps.registry.outputs.host }}
|
||||||
|
username: ${{ steps.registry.outputs.owner }}
|
||||||
|
password: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ steps.registry.outputs.image }}
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: https://github.com/docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=${{ steps.registry.outputs.image }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ steps.registry.outputs.image }}:buildcache,mode=max
|
||||||
26
.github/workflows/main.yaml
vendored
26
.github/workflows/main.yaml
vendored
|
|
@ -1,26 +0,0 @@
|
||||||
name: Build and deploy project
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
env:
|
|
||||||
CONTAINER_REGISTRY: schmelczera
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-project:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout current branch with lfs
|
|
||||||
uses: actions/checkout@master
|
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
- name: Build project
|
|
||||||
run: |
|
|
||||||
npm i && npm run init && npm run build
|
|
||||||
- name: Deploy frontend
|
|
||||||
uses: w9jds/firebase-action@master
|
|
||||||
with:
|
|
||||||
args: deploy --only hosting --project decla-red
|
|
||||||
env:
|
|
||||||
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
|
|
||||||
PROJECT_PATH: frontend
|
|
||||||
50
Dockerfile
Normal file
50
Dockerfile
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# decla.red game server (the `declared-server` package).
|
||||||
|
# The frontend is a static site and is NOT built here — see .forgejo/workflows.
|
||||||
|
|
||||||
|
# ---- Stage 1: build the shared lib, then bundle the server -------------------
|
||||||
|
# Node 14 matches the project toolchain (webpack 4 / TypeScript 4, see
|
||||||
|
# .devcontainer). The full (non-slim) image carries the build tools that
|
||||||
|
# socket.io's optional native deps (bufferutil/utf-8-validate) compile against.
|
||||||
|
FROM node:14-bullseye AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# `shared` is consumed by the backend as `file:../shared` and is bundled into
|
||||||
|
# the server bundle by webpack, so it must be installed and built first.
|
||||||
|
COPY shared/package.json shared/
|
||||||
|
RUN cd shared && npm install
|
||||||
|
COPY shared/ shared/
|
||||||
|
RUN cd shared && npm run build
|
||||||
|
|
||||||
|
# Install the backend deps. `file:../shared` now resolves to the built /app/shared.
|
||||||
|
COPY backend/package.json backend/
|
||||||
|
RUN cd backend && npm install
|
||||||
|
COPY backend/ backend/
|
||||||
|
RUN cd backend && npm run build
|
||||||
|
|
||||||
|
# Drop devDependencies; the runtime only needs the production deps that webpack
|
||||||
|
# left external (express, socket.io, cors, gl-matrix, minimist, msgpack parser).
|
||||||
|
RUN cd backend && npm prune --production
|
||||||
|
|
||||||
|
# ---- Stage 2: minimal runtime ----------------------------------------------
|
||||||
|
FROM node:14-bullseye-slim
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Run as an unprivileged user.
|
||||||
|
RUN groupadd -r app && useradd -r -g app -d /app app
|
||||||
|
|
||||||
|
COPY --from=build /app/backend/dist ./dist
|
||||||
|
COPY --from=build /app/backend/node_modules ./node_modules
|
||||||
|
|
||||||
|
USER app
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Hits the same /state endpoint the website polls (see serverInformationEndpoint).
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "dist/main.js"]
|
||||||
|
# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--worldSize`.
|
||||||
|
CMD ["--port", "3000"]
|
||||||
27
README.md
27
README.md
|
|
@ -1,7 +1,5 @@
|
||||||
# [decla.red](https://decla.red)
|
# [decla.red](https://decla.red)
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
A 2-dimensional multiplayer game utilising ray tracing.
|
A 2-dimensional multiplayer game utilising ray tracing.
|
||||||
|
|
||||||
> **Available at [decla.red](https://decla.red).**
|
> **Available at [decla.red](https://decla.red).**
|
||||||
|
|
@ -10,3 +8,28 @@ A 2-dimensional multiplayer game utilising ray tracing.
|
||||||

|

|
||||||
|
|
||||||
For optimised 2D ray tracing, [SDF-2D](https://github.com/schmelczerandras/sdf-2d) is used.
|
For optimised 2D ray tracing, [SDF-2D](https://github.com/schmelczerandras/sdf-2d) is used.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
CI/CD runs on Forgejo Actions (`.forgejo/workflows/deploy.yml`). On a push to
|
||||||
|
`main` it:
|
||||||
|
|
||||||
|
- builds the static frontend and rsyncs `frontend/dist/` to the `/pages/declared`
|
||||||
|
mount on the runner host, and
|
||||||
|
- builds the server image from the root `Dockerfile` and pushes it to the Forgejo
|
||||||
|
container registry as `<registry>/<owner>/<repo>-server`.
|
||||||
|
|
||||||
|
The registry job needs a `FORGEJO_PACKAGE_TOKEN` secret (with package write
|
||||||
|
scope) and, optionally, a `CONTAINER_REGISTRY_HOST` variable to override the
|
||||||
|
registry host.
|
||||||
|
|
||||||
|
The website's server list is hardcoded in
|
||||||
|
[`frontend/src/scripts/configuration.ts`](frontend/src/scripts/configuration.ts) —
|
||||||
|
edit it to add or remove game-server origins.
|
||||||
|
|
||||||
|
Run the server image locally:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t declared-server .
|
||||||
|
docker run -p 3000:3000 declared-server --name "My server" --playerLimit 16
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
"@types/config": "0.0.36",
|
"@types/config": "0.0.36",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"gl-matrix": "^3.3.0",
|
"gl-matrix": "3.3.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^2.3.0",
|
||||||
"socket.io-msgpack-parser": "^2.0.0",
|
"socket.io-msgpack-parser": "^2.0.0"
|
||||||
"uws": "^10.148.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.7",
|
"@types/cors": "^2.8.7",
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,7 @@
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"common-config-webpack-plugin": "^2.0.1",
|
"common-config-webpack-plugin": "^2.0.1",
|
||||||
"css-loader": "^1.0.1",
|
"css-loader": "^1.0.1",
|
||||||
"firebase": "^7.22.0",
|
"gl-matrix": "3.3.0",
|
||||||
"gl-matrix": "^3.3.0",
|
|
||||||
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
||||||
"html-webpack-inline-svg-plugin": "^2.3.0",
|
"html-webpack-inline-svg-plugin": "^2.3.0",
|
||||||
"html-webpack-plugin": "^4.5.0",
|
"html-webpack-plugin": "^4.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,19 @@
|
||||||
import firebase from 'firebase/app';
|
/**
|
||||||
import 'firebase/firebase-remote-config';
|
* Hardcoded list of game servers the landing page offers to players.
|
||||||
|
*
|
||||||
|
* Each entry is the public origin of a dockerized `declared-server` instance.
|
||||||
|
* The join screen polls `<origin>/state` (see `serverInformationEndpoint`) and
|
||||||
|
* only shows a server once it responds, so listing an offline origin here is
|
||||||
|
* harmless. Add or remove origins as you deploy more server containers.
|
||||||
|
*/
|
||||||
|
const servers: Array<string> = ['https://server.decla.red'];
|
||||||
|
|
||||||
export abstract class Configuration {
|
export abstract class Configuration {
|
||||||
private static remoteConfig: firebase.remoteConfig.RemoteConfig;
|
|
||||||
private static initialized = false;
|
|
||||||
|
|
||||||
public static async initialize(): Promise<void> {
|
public static async initialize(): Promise<void> {
|
||||||
if (this.initialized) {
|
// Kept async for call-site compatibility; the server list is static now.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
|
||||||
apiKey: 'AIzaSyBG85dp-AhaCW-qi_6mu77wDPSipzipIF4',
|
|
||||||
authDomain: 'decla-red.firebaseapp.com',
|
|
||||||
projectId: 'decla-red',
|
|
||||||
appId: '1:635208271441:web:c910843ae7e0549dadda70',
|
|
||||||
};
|
|
||||||
|
|
||||||
firebase.initializeApp(firebaseConfig);
|
|
||||||
|
|
||||||
this.remoteConfig = firebase.remoteConfig();
|
|
||||||
|
|
||||||
this.remoteConfig.settings = {
|
|
||||||
minimumFetchIntervalMillis: 0, // todo: 3600 * 1000,
|
|
||||||
fetchTimeoutMillis: 15 * 1000,
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
await this.remoteConfig.ensureInitialized();
|
|
||||||
await this.remoteConfig.fetchAndActivate();
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get servers(): Array<string> {
|
public static get servers(): Array<string> {
|
||||||
if (!this.initialized) {
|
return servers;
|
||||||
throw new Error('Configuration should be initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(this.remoteConfig.getValue('online_servers').asString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"file-loader": "^6.1.0",
|
"file-loader": "^6.1.0",
|
||||||
"gl-matrix": "^3.3.0",
|
"gl-matrix": "3.3.0",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"terser-webpack-plugin": "^2.3.8",
|
"terser-webpack-plugin": "^2.3.8",
|
||||||
"ts-loader": "^8.0.3",
|
"ts-loader": "^8.0.3",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue