changes
Build and Deploy / build (push) Failing after 41s
Build and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-05-29 19:13:57 +02:00
parent 7de6864f70
commit e732858b8c
11 changed files with 168 additions and 32 deletions
+12
View File
@@ -23,3 +23,15 @@ TLS_KEY_PATH=./certs/key.pem
CORS_ORIGIN=*
# Base URL advertised in the OpenAPI document.
PUBLIC_URL=https://localhost:3000
# --- Icons -------------------------------------------------------------------
# Directory containing the mod .jar files; scanned once on startup to extract
# item / block textures into the icon cache.
MODS_DIR=/mods
# Optional path to the Minecraft client jar. Needed to extract vanilla
# `minecraft:*` textures, since those live in the client jar and not in MODS_DIR.
# Example: VANILLA_JAR=C:/Users/you/AppData/Roaming/.minecraft/versions/1.20.1/1.20.1.jar
VANILLA_JAR=
# Where the extracted PNGs are cached on disk. The cache survives restarts via
# a manifest.json that tracks jar mtime+size.
ICON_CACHE_DIR=/cache/icons
+58
View File
@@ -0,0 +1,58 @@
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login https://gitea.johannesbot.de -u ${{ secrets.REGISTRY_USER }} --password-stdin
- name: Build Image
run: docker build -t gitea.johannesbot.de/johannesbot/mc-computer-craft-api-backend:latest .
- name: Push Image
run: docker push gitea.johannesbot.de/johannesbot/mc-computer-craft-api-backend:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create deployment directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT || 22 }}
script: mkdir -p /home/${{ secrets.SSH_USER }}/mc-computer-craft-api-backend
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT || 22 }}
script: |
cd /home/${{ secrets.SSH_USER }}/mc-computer-craft-api
mv docker-compose.prod.yml docker-compose.yml
# Login as root/sudo to ensure we can pull
echo "${{ secrets.REGISTRY_PASSWORD }}" | sudo docker login https://gitea.johannesbot.de -u ${{ secrets.REGISTRY_USER }} --password-stdin
sudo docker compose pull
sudo docker compose up -d --remove-orphans
sudo docker image prune -f
+4 -1
View File
@@ -10,7 +10,10 @@ lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.idea
package-lock.json
mods/
cache/
# Runtime data
pids
*.pid
+46 -13
View File
@@ -1,10 +1,11 @@
--[[
ME system uploader for ComputerCraft (CC: Tweaked).
Storage system uploader for ComputerCraft (CC: Tweaked).
Requires an "ME Bridge" peripheral (Advanced Peripherals) connected to the
computer and wired to an Applied Energistics 2 / Refined Storage network.
Works with both Advanced Peripherals bridges:
* "meBridge" -> Applied Energistics 2 (listItems / item.amount)
* "rsBridge" -> Refined Storage (getItems / item.count)
It periodically reads the network contents and POSTs them to the backend's
Periodically reads the network contents and POSTs them to the backend's
/api/me-system/update endpoint.
NOTE: the backend serves HTTPS with a self-signed certificate by default.
@@ -14,25 +15,55 @@
* terminate TLS with a proxy that uses a certificate CC trusts.
]]
local API_URL = "http://your-server:3000/api/me-system/update"
local API_URL = "https://192.168.2.150:3000/api/me-system/update"
local INTERVAL = 10 -- seconds between uploads
-- Resolve the bridge peripheral and pick the right method for it.
local bridge = peripheral.find("meBridge")
local listFn
if bridge then
listFn = "listItems"
else
bridge = peripheral.find("rsBridge")
if bridge then
listFn = "getItems"
end
end
if not bridge then
error("No ME Bridge peripheral found")
error("No meBridge or rsBridge peripheral found")
end
if type(bridge[listFn]) ~= "function" then
error("Bridge peripheral does not expose " .. listFn .. "()")
end
local function cleanDisplayName(raw)
if type(raw) ~= "string" then return nil end
-- ME/RS display names are wrapped in [brackets] — strip them.
return (raw:gsub("[%[%]]", ""))
end
-- Bridges report the same item id once per storage cell. Merge the rows here
-- so the backend receives a clean snapshot.
local function collect()
local items = bridge.listItems()
local payload = {}
local items = bridge[listFn](bridge)
local byName = {}
local order = {}
for _, item in ipairs(items) do
payload[#payload + 1] = {
local amount = item.amount or item.count or 0
local entry = byName[item.name]
if entry then
entry.amount = entry.amount + amount
else
entry = {
name = item.name,
amount = item.amount or item.count or 0,
displayName = item.displayName,
amount = amount,
displayName = cleanDisplayName(item.displayName),
}
byName[item.name] = entry
order[#order + 1] = entry
end
return payload
end
return order
end
local function upload(payload)
@@ -48,7 +79,9 @@ local function upload(payload)
response.close()
end
print("ME uploader started. Target: " .. API_URL)
print("Storage uploader started.")
print(" Bridge: " .. listFn)
print(" Target: " .. API_URL)
while true do
local ok, payload = pcall(collect)
if ok then
+4 -1
View File
@@ -10,7 +10,8 @@
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test:update-loop": "tsx test/update-loop.ts"
},
"dependencies": {
"@fastify/cors": "^10.0.1",
@@ -19,12 +20,14 @@
"@fastify/type-provider-typebox": "^5.1.0",
"@fastify/websocket": "^11.0.1",
"@sinclair/typebox": "^0.34.9",
"adm-zip": "^0.5.17",
"dotenv": "^16.4.7",
"fastify": "^5.2.0",
"pg": "^8.13.1",
"selfsigned": "^2.4.1"
},
"devDependencies": {
"@types/adm-zip": "^0.5.8",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.10",
"tsx": "^4.19.2",
+11
View File
@@ -15,6 +15,11 @@ function readBool(key: string, fallback: boolean): boolean {
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}
function readOptional(key: string): string | null {
const value = process.env[key];
return value === undefined || value === '' ? null : value;
}
export const config = {
/** Interface the HTTP/WS server binds to. */
host: readEnv('HOST', '0.0.0.0'),
@@ -34,6 +39,12 @@ export const config = {
corsOrigin: readEnv('CORS_ORIGIN', '*'),
/** Public base URL advertised in the OpenAPI document. */
publicUrl: readEnv('PUBLIC_URL', 'https://localhost:3000'),
/** Directory containing mod `.jar` files; scanned for item textures on startup. */
modsDir: readEnv('MODS_DIR', './mods'),
/** Optional path to the Minecraft client jar to extract vanilla `minecraft:*` textures from. */
vanillaJar: readOptional('VANILLA_JAR'),
/** Directory where extracted item PNGs are cached on disk. */
iconCacheDir: readEnv('ICON_CACHE_DIR', './cache/icons'),
} as const;
export type AppConfig = typeof config;
+3
View File
@@ -1,12 +1,15 @@
import { config } from './config.js';
import { closePool } from './db/pool.js';
import { runMigrations } from './db/migrate.js';
import { iconCount, loadIcons } from './lib/iconRegistry.js';
import { buildServer } from './server.js';
async function main(): Promise<void> {
await runMigrations();
await loadIcons();
const app = await buildServer();
app.log.info(`Icon registry ready: ${iconCount()} item textures available`);
await app.listen({ host: config.host, port: config.port });
const scheme = config.tls.enabled ? 'https' : 'http';
+5 -1
View File
@@ -1,4 +1,5 @@
import { pool } from '../db/pool.js';
import { iconUrl } from '../lib/iconRegistry.js';
/* ------------------------------------------------------------------ */
/* Types */
@@ -10,6 +11,7 @@ export interface LiveItem {
mod: string;
amount: number;
updatedAt: string;
icon: string | null;
}
export interface ModInfo {
@@ -59,12 +61,14 @@ function toIso(value: unknown): string {
}
function rowToLiveItem(row: Record<string, unknown>): LiveItem {
const name = row.name as string;
return {
name: row.name as string,
name,
displayName: (row.display_name as string | null) ?? null,
mod: row.mod as string,
amount: Number(row.amount),
updatedAt: toIso(row.updated_at),
icon: iconUrl(name),
};
}
+3
View File
@@ -38,6 +38,9 @@ export const LiveItemSchema = Type.Object({
mod: Type.String(),
amount: Type.Integer(),
updatedAt: Type.String({ format: 'date-time' }),
icon: Type.Union([Type.String(), Type.Null()], {
description: 'URL to a 64×64 PNG, or null when no texture is available.',
}),
});
export const ModInfoSchema = Type.Object({
+5 -8
View File
@@ -7,6 +7,7 @@ import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { config } from './config.js';
import { ensureTlsMaterial } from './lib/tls.js';
import { healthRoutes } from './routes/health.js';
import { iconRoutes } from './routes/icon.js';
import { meSystemRoutes } from './routes/meSystem.js';
import { wsRoutes } from './routes/ws.js';
@@ -19,17 +20,12 @@ export async function buildServer(): Promise<FastifyInstance> {
? ensureTlsMaterial(config.tls.certPath, config.tls.keyPath)
: null;
const baseOptions = {
const app = Fastify({
logger: { level: process.env.LOG_LEVEL ?? 'info' },
// ME systems can hold thousands of distinct items in one snapshot.
bodyLimit: 16 * 1024 * 1024,
};
const app = (
tls
? Fastify({ ...baseOptions, https: { key: tls.key, cert: tls.cert } })
: Fastify(baseOptions)
).withTypeProvider<TypeBoxTypeProvider>();
...(tls ? { https: { key: tls.key, cert: tls.cert } } : {}),
}).withTypeProvider<TypeBoxTypeProvider>();
await app.register(cors, { origin: config.corsOrigin });
@@ -56,6 +52,7 @@ export async function buildServer(): Promise<FastifyInstance> {
await app.register(healthRoutes);
await app.register(meSystemRoutes);
await app.register(iconRoutes);
await app.register(wsRoutes);
return app;
+11 -2
View File
@@ -24,12 +24,21 @@ let lastHistoryUpdate = 0;
* data to all connected websocket clients.
*/
export async function processUpdate(items: RawUpdateItem[]): Promise<UpdateResult> {
const normalized: repo.NormalizedItem[] = items.map((item) => ({
const seen = new Map<string, repo.NormalizedItem>();
for (const item of items) {
const existing = seen.get(item.name);
if (existing) {
existing.amount += item.amount;
} else {
seen.set(item.name, {
name: item.name,
amount: item.amount,
displayName: item.displayName ?? null,
mod: modFromName(item.name),
}));
});
}
}
const normalized = [...seen.values()];
await repo.upsertLive(normalized);