server for refapp and refbot and other stuff
0

Configure Feed

Select the types of activity you want to include in your feed.

build(refserver): add Dockerfile + release pipeline (Portainer deploy)

Mirrors refapp/refbot: Dockerfile builds the TS image (exposes PORT 3000 +
WS_PORT 3002), scripts/release.js bumps version, tags/pushes git, then builds
and pushes registry.ellite.dev/alpinesystem/refserver:{latest,vX.Y.Z} via the
Portainer Docker API. PORTAINER_STACK_ID gates the redeploy step so the image
is pushed safely before the stack exists; REGISTRY_PASSWORD + PORTAINER_TOKEN
are required for the build/push.

alpine (Jul 15, 2026, 9:21 AM +0200) cac17878 eb059c87

+188 -1
+15
.dockerignore
··· 1 + node_modules 2 + dist 3 + .git 4 + .gitignore 5 + .env* 6 + *.md 7 + *.log 8 + npm-debug.log* 9 + .DS_Store 10 + .dockerignore 11 + .vscode 12 + .tangled 13 + .claude 14 + coverage 15 + Dockerfile
+8
Dockerfile
··· 1 + FROM node:alpine 2 + WORKDIR /usr/src/app 3 + COPY refserver/package*.json ./ 4 + RUN npm ci --silent --no-fund 5 + COPY refserver/ . 6 + RUN npm run build 7 + EXPOSE 3000 3002 8 + CMD ["node", "dist/index.js"]
+2 -1
package.json
··· 12 12 "typecheck": "tsc --noEmit", 13 13 "test": "npm run build && node --test dist/**/*.test.js", 14 14 "test:unit": "tsx --test src/**/*.test.ts", 15 - "test:watch": "tsx watch --test src/**/*.test.ts" 15 + "test:watch": "tsx watch --test src/**/*.test.ts", 16 + "release": "node scripts/release.js" 16 17 }, 17 18 "keywords": [], 18 19 "author": "",
+163
scripts/release.js
··· 1 + import fs from 'node:fs'; 2 + import path from 'node:path'; 3 + import os from 'node:os'; 4 + import { fileURLToPath } from 'node:url'; 5 + import { execSync } from 'node:child_process'; 6 + 7 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 + const pkgPath = path.join(__dirname, '../package.json'); 9 + 10 + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); 11 + const oldVersion = pkg.version; 12 + const argVersion = process.argv[2]; 13 + let newVersion; 14 + if (argVersion) { 15 + newVersion = argVersion.replace(/^v/, ''); 16 + } 17 + else { 18 + const [major, minor, patch] = pkg.version.split('.').map(Number); 19 + newVersion = `${major}.${minor}.${patch + 1}`; 20 + } 21 + 22 + pkg.version = newVersion; 23 + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); 24 + 25 + console.log(`Bumped version: ${oldVersion} → ${newVersion}`); 26 + 27 + execSync('git add package.json', { stdio: 'inherit' }); 28 + execSync(`git commit -m 'chore: bump version to v${newVersion}'`, { stdio: 'inherit' }); 29 + execSync(`git tag v${newVersion}`, { stdio: 'inherit' }); 30 + execSync('git push --tags', { stdio: 'inherit' }); 31 + execSync('git push', { stdio: 'inherit' }); 32 + 33 + console.log(`Tagged and pushed v${newVersion}`); 34 + 35 + const registryPassword = process.env.REGISTRY_PASSWORD; 36 + const portainerToken = process.env.PORTAINER_TOKEN; 37 + const registry = 'registry.ellite.dev/alpinesystem/refserver'; 38 + 39 + if (!registryPassword || !portainerToken) { 40 + console.log('REGISTRY_PASSWORD or PORTAINER_TOKEN not set, skipping image build + redeploy.'); 41 + process.exit(0); 42 + } 43 + 44 + const portainerBase = 'https://portainer.ellite.dev/api'; 45 + const endpointId = 2; 46 + // The refserver Portainer stack must be created before automated redeploys can 47 + // work. Set PORTAINER_STACK_ID once the stack exists; until then the script 48 + // still builds and pushes the image but skips the redeploy step. 49 + const stackId = process.env.PORTAINER_STACK_ID 50 + ? Number(process.env.PORTAINER_STACK_ID) 51 + : null; 52 + const headers = { 'X-API-Key': portainerToken, 'Content-Type': 'application/json' }; 53 + 54 + // build context tar — exclude the usual junk 55 + const tarPath = path.join(os.tmpdir(), `refserver-build-${Date.now()}.tar.gz`); 56 + const repoRoot = path.join(__dirname, '..'); 57 + const workspaceRoot = path.join(repoRoot, '..'); 58 + console.log('\nCreating build context...'); 59 + execSync( 60 + `tar -czf ${tarPath} --exclude='*/node_modules' --exclude='*/dist' --exclude='*/.git' --exclude='*/.env' -C ${workspaceRoot} refserver`, 61 + { stdio: 'inherit' }, 62 + ); 63 + 64 + // build on the portainer server's docker daemon via the docker api proxy 65 + console.log('\nBuilding image on server...'); 66 + const buildUrl = `${portainerBase}/endpoints/${endpointId}/docker/build` 67 + + `?t=${encodeURIComponent(`${registry}:latest`)}` 68 + + `&t=${encodeURIComponent(`${registry}:v${newVersion}`)}` 69 + + `&dockerfile=refserver/Dockerfile`; 70 + 71 + const buildRes = await fetch(buildUrl, { 72 + method: 'POST', 73 + headers: { 74 + 'X-API-Key': portainerToken, 75 + 'Content-Type': 'application/x-tar', 76 + }, 77 + body: fs.readFileSync(tarPath), 78 + }); 79 + 80 + fs.unlinkSync(tarPath); 81 + 82 + if (!buildRes.ok) { 83 + console.error(`Build failed: ${buildRes.status} ${await buildRes.text()}`); 84 + process.exit(1); 85 + } 86 + 87 + // docker build streams back newline-delimited json — print it 88 + const buildText = await buildRes.text(); 89 + for (const line of buildText.split('\n')) { 90 + if (!line.trim()) continue; 91 + try { 92 + const obj = JSON.parse(line); 93 + if (obj.stream) process.stdout.write(obj.stream); 94 + else if (obj.error) { console.error('Build error:', obj.error); process.exit(1); } 95 + else if (obj.aux) console.log('aux:', JSON.stringify(obj.aux)); 96 + } 97 + catch { process.stdout.write(line + '\n'); } 98 + } 99 + 100 + // push both tags — X-Registry-Auth is base64({"username":...,"password":...,"serveraddress":...}) 101 + const registryAuth = Buffer.from(JSON.stringify({ 102 + username: 'refbot', 103 + password: registryPassword, 104 + serveraddress: 'registry.ellite.dev', 105 + })).toString('base64'); 106 + 107 + const pushHeaders = { 'X-API-Key': portainerToken, 'X-Registry-Auth': registryAuth }; 108 + 109 + for (const tag of ['latest', `v${newVersion}`]) { 110 + console.log(`\nPushing ${registry}:${tag}...`); 111 + const imageName = encodeURIComponent(`${registry}:${tag}`); 112 + const pushRes = await fetch( 113 + `${portainerBase}/endpoints/${endpointId}/docker/images/${imageName}/push`, 114 + { method: 'POST', headers: pushHeaders }, 115 + ); 116 + 117 + if (!pushRes.ok) { 118 + console.error(`Push failed (${tag}): ${pushRes.status} ${await pushRes.text()}`); 119 + process.exit(1); 120 + } 121 + 122 + const pushText = await pushRes.text(); 123 + for (const line of pushText.split('\n')) { 124 + if (!line.trim()) continue; 125 + try { 126 + const obj = JSON.parse(line); 127 + if (obj.status) console.log(obj.status, obj.progress ?? ''); 128 + else if (obj.error) { console.error('Push error:', obj.error); process.exit(1); } 129 + } 130 + catch { process.stdout.write(line + '\n'); } 131 + } 132 + } 133 + 134 + if (!stackId) { 135 + console.log('\nPORTAINER_STACK_ID not set, skipping stack redeploy. ' 136 + + 'Image pushed; create the refserver stack and set PORTAINER_STACK_ID to enable redeploys.'); 137 + process.exit(0); 138 + } 139 + 140 + // redeploy the portainer stack 141 + console.log('\nUpdating Portainer stack...'); 142 + const [envRes, fileRes] = await Promise.all([ 143 + fetch(`${portainerBase}/stacks/${stackId}`, { headers }), 144 + fetch(`${portainerBase}/stacks/${stackId}/file`, { headers }), 145 + ]); 146 + 147 + const currentEnv = (await envRes.json()).Env ?? []; 148 + const composeContent = (await fileRes.json()).StackFileContent; 149 + 150 + const updateRes = await fetch(`${portainerBase}/stacks/${stackId}?endpointId=${endpointId}`, { 151 + method: 'PUT', 152 + headers, 153 + body: JSON.stringify({ stackFileContent: composeContent, env: currentEnv, pullImage: true }), 154 + }); 155 + 156 + if (updateRes.ok) { 157 + console.log('Portainer stack updated.'); 158 + } 159 + else { 160 + const body = await updateRes.text(); 161 + console.error(`Portainer update failed: ${updateRes.status} ${body}`); 162 + process.exit(1); 163 + }