fix: modernize docker publishing

This commit is contained in:
2026-04-29 13:53:20 +00:00
parent e0ba779ac2
commit 86dafd6c5b
16 changed files with 4990 additions and 1911 deletions
+4
View File
@@ -1 +1,5 @@
.git/
.nogit/
dist/
dist_*/
node_modules/ node_modules/
+36
View File
@@ -0,0 +1,36 @@
name: Docker (non-tag pushes)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:szci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
test:
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @git.zone/tsdocker@latest
pnpm config set registry https://verdaccio.lossless.digital
pnpm install
- name: Test
run: pnpm test
- name: Build image
run: tsdocker build
- name: Test image
run: tsdocker test
+42
View File
@@ -0,0 +1,42 @@
name: Docker (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:szci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
release:
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-dbase:szci
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @git.zone/tsdocker@latest
pnpm config set registry https://verdaccio.lossless.digital
pnpm install
- name: Login to registries
run: tsdocker login
- name: List images
run: tsdocker list
- name: Build images
run: tsdocker build
- name: Test images
run: tsdocker test
- name: Push to code.foss.global
run: tsdocker push code.foss.global
+8 -11
View File
@@ -1,23 +1,20 @@
{ {
"npmci": { "@git.zone/tsdocker": {
"npmGlobalTools": [], "registries": ["code.foss.global"],
"dockerRegistryRepoMap": { "registryRepoMap": {
"code.foss.global": "serve.zone/corerender" "code.foss.global": "serve.zone/corerender"
}, },
"dockerBuildargEnvMap": { "platforms": ["linux/amd64", "linux/arm64"]
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
},
"npmRegistryUrl": "verdaccio.lossless.digital"
}, },
"gitzone": { "@git.zone/cli": {
"projectType": "service", "projectType": "service",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
"gitscope": "serve.zone", "gitscope": "serve.zone",
"gitrepo": "corerender", "gitrepo": "corerender",
"description": "A rendering service designed for serve.zone that efficiently preserves styles while rendering web components.", "description": "A rendering service designed for serve.zone that efficiently preserves styles while rendering web components.",
"npmPackagename": "@serve.zone/corerender", "npmPackagename": "corerender",
"license": "MIT", "license": "UNLICENSED",
"keywords": [ "keywords": [
"rendering", "rendering",
"web components", "web components",
@@ -35,4 +32,4 @@
] ]
} }
} }
} }
+27 -28
View File
@@ -1,35 +1,34 @@
# gitzone dockerfile_service # gitzone dockerfile_service
## STAGE 1 // BUILD ## STAGE 1 // BUILD
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1 FROM code.foss.global/host.today/ht-docker-node:lts AS build
COPY ./ /app
WORKDIR /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN rm -rf node_modules && npm install
RUN npm run build
# gitzone dockerfile_service
## STAGE 2 // install production
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2
WORKDIR /app WORKDIR /app
COPY --from=node1 /app /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN rm -r node_modules/ && npm install --production
### Healthchecks COPY package.json pnpm-lock.yaml ./
RUN npm install -g @servezone/healthy RUN pnpm config set store-dir .pnpm-store
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ] RUN pnpm config set registry https://verdaccio.lossless.digital
RUN pnpm install --frozen-lockfile
COPY . ./
RUN pnpm run build
RUN rm -rf .pnpm-store
RUN pnpm prune --prod
## STAGE 2 // PRODUCTION
FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/cli.js ./cli.js
COPY --from=build /app/dist_ts ./dist_ts
LABEL org.opencontainers.image.title="corerender" \
org.opencontainers.image.description="serve.zone web component rendering service" \
org.opencontainers.image.source="https://code.foss.global/serve.zone/corerender"
EXPOSE 80 EXPOSE 80
CMD ["node", "cli.js"]
# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
# CMD
CMD ["npm", "start"]
+23 -19
View File
@@ -12,30 +12,34 @@
"startTs": "(node cli.ts.js)", "startTs": "(node cli.ts.js)",
"watch": "tswatch service", "watch": "tswatch service",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc",
"build:docker": "tsdocker build --verbose",
"release:docker": "tsdocker push --verbose"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.66", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsdocker": "^2.2.4",
"@git.zone/tstest": "^1.0.77", "@git.zone/tsrun": "^2.0.2",
"@git.zone/tswatch": "^2.0.7", "@git.zone/tstest": "^3.6.3",
"@push.rocks/tapbundle": "^5.0.8" "@git.zone/tswatch": "^3.3.2",
"@push.rocks/tapbundle": "^5.0.8",
"@types/node": "^25.6.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedserver": "^3.0.53", "@api.global/typedserver": "^8.4.6",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/smartdata": "^5.0.14", "@push.rocks/smartdata": "^7.1.7",
"@push.rocks/smartdelay": "^3.0.1", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartlog": "^3.0.7", "@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartpath": "^5.0.5", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.2", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.15", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrobots": "^1.0.2", "@push.rocks/smartrobots": "^1.0.2",
"@push.rocks/smartsitemap": "^2.0.1", "@push.rocks/smartsitemap": "^4.0.1",
"@push.rocks/smartssr": "^1.0.40", "@push.rocks/smartssr": "^1.0.40",
"@push.rocks/smartstate": "^2.0.6", "@push.rocks/smartstate": "^2.3.0",
"@push.rocks/smarttime": "^4.0.1", "@push.rocks/smarttime": "^4.2.3",
"@push.rocks/taskbuffer": "^3.0.10" "@push.rocks/taskbuffer": "^8.0.2"
}, },
"private": true, "private": true,
"files": [ "files": [
@@ -47,7 +51,7 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"browserslist": [ "browserslist": [
+4762 -1788
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
node --input-type=module <<'NODE'
import fs from 'node:fs';
const readJson = (path) => JSON.parse(fs.readFileSync(path, 'utf8'));
const checks = {
packageVersion: readJson('/app/package.json').version,
hasCli: fs.existsSync('/app/cli.js'),
hasIndex: fs.existsSync('/app/dist_ts/index.js'),
hasRendertronClass: fs.existsSync('/app/dist_ts/rendertron.classes.rendertron.js'),
};
if (checks.packageVersion !== '2.0.62') {
throw new Error(`Unexpected corerender package version ${checks.packageVersion}`);
}
if (!checks.hasCli) {
throw new Error('Missing cli.js');
}
if (!checks.hasIndex) {
throw new Error('Missing dist_ts/index.js');
}
if (!checks.hasRendertronClass) {
throw new Error('Missing rendertron class output');
}
console.log(JSON.stringify(checks));
NODE
+2 -3
View File
@@ -10,13 +10,12 @@ let rendertronInstance: Rendertron;
export const runCli = async () => { export const runCli = async () => {
logger.log('info', `Starting rendertron...`); logger.log('info', `Starting rendertron...`);
rendertronInstance = new Rendertron(); rendertronInstance = new Rendertron();
rendertronInstance.start(); await rendertronInstance.start();
logger.log('success', `Successfully started rendertron!`); logger.log('success', `Successfully started rendertron!`);
}; };
export const stop = async () => { export const stop = async () => {
if (rendertronInstance) { if (rendertronInstance) {
rendertronInstance.stop(); await rendertronInstance.stop();
} }
db.close();
}; };
+16 -13
View File
@@ -3,9 +3,9 @@ import { PrerenderResult } from './rendertron.classes.prerenderresult.js';
import * as plugins from './rendertron.plugins.js'; import * as plugins from './rendertron.plugins.js';
export class PrerenderManager { export class PrerenderManager {
public smartssrInstance: plugins.smartssr.SmartSSR; public smartssrInstance!: plugins.smartssr.SmartSSR;
public smartrobotsInstance: plugins.smartrobots.Smartrobots; public smartrobotsInstance!: plugins.smartrobots.Smartrobots;
public smartsitemapInstance: plugins.smartsitemap.SmartSitemap; public smartsitemapInstance!: plugins.smartsitemap.SmartSitemap;
constructor() {} constructor() {}
@@ -31,6 +31,9 @@ export class PrerenderManager {
return null; return null;
} }
); );
if (!prerenderResult) {
return done.promise;
}
done.resolve(prerenderResult.renderResultString); done.resolve(prerenderResult.renderResultString);
return done.promise; return done.promise;
} }
@@ -64,19 +67,19 @@ export class PrerenderManager {
public async prerenderSitemap(sitemapUrlArg: string) { public async prerenderSitemap(sitemapUrlArg: string) {
logger.log('info', `prerendering sitemap: ${sitemapUrlArg}`); logger.log('info', `prerendering sitemap: ${sitemapUrlArg}`);
const parsedSitemap = await this.smartsitemapInstance.parseSitemapUrl(sitemapUrlArg); const parsedSitemap = await plugins.smartsitemap.SmartSitemap.parseUrl(sitemapUrlArg);
if (!parsedSitemap.urlset?.url) { if (parsedSitemap.type === 'sitemapindex') {
for (const sitemap of parsedSitemap.sitemaps) {
await this.prerenderSitemap(sitemap.loc);
}
return; return;
} }
if (!(parsedSitemap.urlset.url instanceof Array)) {
await this.getPrerenderResultForUrl(parsedSitemap.urlset.url.loc); for (const url of parsedSitemap.urls) {
} else { if (!url?.loc) {
for (const url of parsedSitemap.urlset.url) { continue;
if (!url?.loc) {
continue;
}
await this.getPrerenderResultForUrl(url.loc);
} }
await this.getPrerenderResultForUrl(url.loc);
} }
} }
+3 -3
View File
@@ -77,13 +77,13 @@ export class PrerenderResult extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE // INSTANCE
@plugins.smartdata.unI() @plugins.smartdata.unI()
url: string; url!: string;
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
renderResultString: string; renderResultString!: string;
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
timestamp: number; timestamp!: number;
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
needsRerendering: boolean = false; needsRerendering: boolean = false;
+37 -38
View File
@@ -1,21 +1,23 @@
import * as plugins from './rendertron.plugins.js'; import * as plugins from './rendertron.plugins.js';
import * as paths from './rendertron.paths.js'; import * as paths from './rendertron.paths.js';
import { logger } from './rendertron.logging.js'; import { logger } from './rendertron.logging.js';
import { db } from './rendertron.db.js';
import { PrerenderResult } from './rendertron.classes.prerenderresult.js'; import { PrerenderResult } from './rendertron.classes.prerenderresult.js';
import { PrerenderManager } from './rendertron.classes.prerendermanager.js'; import { PrerenderManager } from './rendertron.classes.prerendermanager.js';
import { TaskManager } from './rendertron.taskmanager.js'; import { TaskManager } from './rendertron.taskmanager.js';
export class Rendertron { export class Rendertron {
public projectinfo: plugins.projectinfo.ProjectInfo; public projectinfo!: plugins.projectinfo.ProjectInfo;
public serviceServerInstance: plugins.typedserver.utilityservers.UtilityServiceServer; public serviceServerInstance!: plugins.typedserver.utilityservers.UtilityServiceServer;
public prerenderManager: PrerenderManager; public prerenderManager!: PrerenderManager;
public taskManager: TaskManager; public taskManager!: TaskManager;
/** /**
* starts the financeflow instance * starts the financeflow instance
*/ */
public async start() { public async start() {
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir); this.projectinfo = await plugins.projectinfo.ProjectInfo.create(paths.packageDir);
await db.init();
this.prerenderManager = new PrerenderManager(); this.prerenderManager = new PrerenderManager();
this.taskManager = new TaskManager(this); this.taskManager = new TaskManager(this);
await this.prerenderManager.start(); await this.prerenderManager.start();
@@ -27,55 +29,49 @@ export class Rendertron {
addCustomRoutes: async (serverArg) => { addCustomRoutes: async (serverArg) => {
serverArg.addRoute( serverArg.addRoute(
'/render/*', '/render/*',
new plugins.typedserver.servertools.Handler('GET', async (req, res) => { 'GET',
const requestedUrl = req.url.replace('/render/', ''); async (ctxArg) => {
const requestedUrl = `${ctxArg.url.pathname.replace('/render/', '')}${ctxArg.url.search}`;
logger.log('info', `Got SSR request for ${requestedUrl}`); logger.log('info', `Got SSR request for ${requestedUrl}`);
if (requestedUrl.startsWith('https://url(')) { if (requestedUrl.startsWith('https://url(')) {
logger.log('warn', `relative url error for ${requestedUrl}`); logger.log('warn', `relative url error for ${requestedUrl}`);
res.status(500); return new Response('error due to relative protocol', { status: 500 });
res.write('error due to relative protocol');
res.end();
return;
} }
const originResponse = await plugins.smartrequest const originResponse = await plugins.smartrequest.SmartRequest.create()
.request( .url(requestedUrl)
requestedUrl, .options({ keepAlive: false })
{ .get()
method: 'GET',
keepAlive: false,
// headers: req.headers,
},
true
)
.catch((error) => { .catch((error) => {
logger.log('warn', `the origin request errored for ${requestedUrl}`); logger.log('warn', `the origin request errored for ${requestedUrl}`);
res.write(`rendertron encountered an error for ${requestedUrl}`); return null;
res.end();
}); });
if (!originResponse) { if (!originResponse) {
return; return new Response(`rendertron encountered an error for ${requestedUrl}`, { status: 502 });
} }
for (const header of Object.keys(originResponse.headers)) { const responseHeaders = new Headers();
res.setHeader(header, originResponse.headers[header]); for (const [headerKey, headerValue] of Object.entries(originResponse.headers)) {
if (Array.isArray(headerValue)) {
for (const headerValueItem of headerValue) {
responseHeaders.append(headerKey, headerValueItem);
}
} else if (headerValue !== undefined) {
responseHeaders.set(headerKey, headerValue.toString());
}
} }
if (originResponse.headers['content-type']?.includes('text/html')) { if (originResponse.headers['content-type']?.includes('text/html')) {
logger.log('info', `Piping ${requestedUrl} through smartssr.`); logger.log('info', `Piping ${requestedUrl} through smartssr.`);
res.write(await this.prerenderManager.getPrerenderResultForUrl(requestedUrl)); return new Response(await this.prerenderManager.getPrerenderResultForUrl(requestedUrl), {
res.end(); status: originResponse.status,
headers: responseHeaders,
});
} else { } else {
logger.log('info', `Serving ${requestedUrl} directly.`); logger.log('info', `Serving ${requestedUrl} directly.`);
for (const headerKey of Object.keys(originResponse.headers)) { return new Response(await originResponse.arrayBuffer(), {
console.log(`${headerKey}: ${originResponse.headers[headerKey]}`); status: originResponse.status,
res.set(headerKey, originResponse.headers[headerKey]); headers: responseHeaders,
}
originResponse.on('data', (data) => {
res.write(data);
});
originResponse.on('end', () => {
res.end();
}); });
} }
}) }
); );
}, },
}); });
@@ -86,5 +82,8 @@ export class Rendertron {
this.serviceServerInstance ? await this.serviceServerInstance.stop() : null; this.serviceServerInstance ? await this.serviceServerInstance.stop() : null;
this.prerenderManager ? await this.prerenderManager.stop() : null; this.prerenderManager ? await this.prerenderManager.stop() : null;
this.taskManager ? await this.taskManager.stop() : null; this.taskManager ? await this.taskManager.stop() : null;
if (db.status === 'connected') {
await db.close();
}
} }
} }
-2
View File
@@ -7,5 +7,3 @@ export const db = new plugins.smartdata.SmartdataDb({
mongoDbPass: 'wxW4LBa3sxPjyXGf', mongoDbPass: 'wxW4LBa3sxPjyXGf',
mongoDbUser: 'rendertron', mongoDbUser: 'rendertron',
}); });
db.init();
-4
View File
@@ -1,8 +1,4 @@
import * as plugins from './rendertron.plugins.js'; import * as plugins from './rendertron.plugins.js';
import * as paths from './rendertron.paths.js';
const projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
import { commitinfo } from './00_commitinfo_data.js'; import { commitinfo } from './00_commitinfo_data.js';
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
+1 -1
View File
@@ -16,7 +16,7 @@ export class TaskManager {
logger.log('info', `starting domain prerender in 5 seconds`); logger.log('info', `starting domain prerender in 5 seconds`);
await plugins.smartdelay.delayFor(5000); await plugins.smartdelay.delayFor(5000);
// get projects from lele-pubapiclient // get projects from lele-pubapiclient
const localDomains = []; // TODO: get from coreflow const localDomains: Array<{ name: string; url: string }> = []; // TODO: get from coreflow
for (const project of localDomains) { for (const project of localDomains) {
logger.log('info', `Prerending project ${project.name} with url ${project.url}`); logger.log('info', `Prerending project ${project.name} with url ${project.url}`);
const startTime = Date.now(); const startTime = Date.now();
-1
View File
@@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false, "useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",