import assert from 'assert/strict'; import { existsSync, readFileSync, readdirSync } from 'fs'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); const readWorkspaceFile = (pathFromRoot: string) => { return readFileSync(resolve(root, pathFromRoot), 'utf8'); }; const readWorkspaceJson = (pathFromRoot: string) => { return JSON.parse(readWorkspaceFile(pathFromRoot)) as T; }; const assertIncludes = (filePath: string, expected: string) => { const content = readWorkspaceFile(filePath); assert.ok(content.includes(expected), `${filePath} should include ${expected}`); }; const assertExcludes = (filePath: string, forbidden: string) => { const content = readWorkspaceFile(filePath); assert.ok(!content.includes(forbidden), `${filePath} should not include ${forbidden}`); }; const assertPort = (valueArg: unknown, labelArg: string) => { assert.ok(Number.isInteger(valueArg), `${labelArg} should be an integer`); assert.ok((valueArg as number) >= 1 && (valueArg as number) <= 65535, `${labelArg} should be a valid TCP/UDP port`); }; const assertValidPublishedPort = (portArg: any, labelArg: string) => { assertPort(portArg.targetPort, `${labelArg}.targetPort`); const targetEnd = portArg.targetPortEnd ?? portArg.targetPort; assertPort(targetEnd, `${labelArg}.targetPortEnd`); assert.ok(targetEnd >= portArg.targetPort, `${labelArg} target range should be ascending`); const publishedStart = portArg.publishedPort ?? portArg.targetPort; const publishedEnd = portArg.publishedPortEnd ?? (publishedStart + (targetEnd - portArg.targetPort)); assertPort(publishedStart, `${labelArg}.publishedPort`); assertPort(publishedEnd, `${labelArg}.publishedPortEnd`); assert.ok(publishedEnd >= publishedStart, `${labelArg} published range should be ascending`); assert.equal(targetEnd - portArg.targetPort, publishedEnd - publishedStart, `${labelArg} target and published ranges should have the same size`); assert.ok(['tcp', 'udp'].includes(portArg.protocol ?? 'tcp'), `${labelArg}.protocol should be tcp or udp`); }; const assertValidAppCatalog = () => { const catalog = readWorkspaceJson<{ schemaVersion: number; apps: Array<{ id: string; latestVersion: string }>; }>('appstore-apptemplates/catalog.json'); assert.equal(catalog.schemaVersion, 1, 'App catalog schema version should be 1'); const appIds = catalog.apps.map((appArg) => appArg.id); assert.equal(new Set(appIds).size, appIds.length, 'App catalog ids should be unique'); const appDirs = readdirSync(resolve(root, 'appstore-apptemplates/apps'), { withFileTypes: true }) .filter((direntArg) => direntArg.isDirectory()) .map((direntArg) => direntArg.name) .sort(); assert.deepEqual([...appIds].sort(), appDirs, 'Every app directory should be represented in catalog.json'); for (const catalogApp of catalog.apps) { const appPath = `appstore-apptemplates/apps/${catalogApp.id}`; const metaPath = `${appPath}/app.json`; assert.ok(existsSync(resolve(root, metaPath)), `${metaPath} should exist`); const appMeta = readWorkspaceJson<{ id: string; latestVersion: string; versions: string[]; }>(metaPath); assert.equal(appMeta.id, catalogApp.id, `${metaPath} id should match its directory and catalog entry`); assert.equal(appMeta.latestVersion, catalogApp.latestVersion, `${metaPath} latestVersion should match catalog.json`); assert.ok(appMeta.versions.includes(appMeta.latestVersion), `${metaPath} versions should include latestVersion`); const configPath = `${appPath}/versions/${appMeta.latestVersion}/config.json`; assert.ok(existsSync(resolve(root, configPath)), `${configPath} should exist`); const config = readWorkspaceJson<{ image: string; port: number; envVars?: Array<{ key: string; value?: string; description: string; required?: boolean }>; volumes?: Array; publishedPorts?: any[]; }>(configPath); assert.ok(config.image, `${configPath} should define an image`); assertPort(config.port, `${configPath}.port`); const envKeys = new Set(); for (const envVar of config.envVars || []) { assert.ok(envVar.key, `${configPath} env var should define a key`); assert.ok(!envKeys.has(envVar.key), `${configPath} env var ${envVar.key} should be unique`); envKeys.add(envVar.key); assert.equal(typeof envVar.description, 'string', `${configPath} env var ${envVar.key} should describe its purpose`); } for (const [index, volume] of (config.volumes || []).entries()) { const mountPath = typeof volume === 'string' ? volume : volume.mountPath; assert.ok(mountPath?.startsWith('/'), `${configPath} volume ${index} should define an absolute mountPath`); } for (const [index, publishedPort] of (config.publishedPorts || []).entries()) { assertValidPublishedPort(publishedPort, `${configPath}.publishedPorts[${index}]`); } } }; assertIncludes('containerarchive/rust/src/prune.rs', 'rewritten_offsets'); assertExcludes('containerarchive/rust/src/prune.rs', 'offset: entry.offset, // Note: offset in the new pack may differ'); assertIncludes('containerarchive/test/test.ts', 'partial-pack GC rewrites chunks'); assertExcludes('onebox/ts/classes/onebox.ts', "hashPassword('admin')"); assertIncludes('onebox/ts/classes/onebox.ts', 'ONEBOX_ADMIN_PASSWORD'); assertExcludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', "password: 'admin'"); assertIncludes('dcrouter/ts/opsserver/handlers/admin.handler.ts', 'DCROUTER_ADMIN_PASSWORD'); assertExcludes('corerender/ts/rendertron.db.ts', 'wxW4LBa3sxPjyXGf'); assertExcludes('corerender/ts/rendertron.db.ts', 'losslessone-main.zee8suk.mongodb.net'); assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'return response;'); assertIncludes('api/ts/classes.image.ts', 'versionString: imageVersion'); assertIncludes('cloudly/ts/manager.image/classes.imagemanager.ts', 'readFromWebstream(readable)'); assertIncludes('cloudly/ts/manager.service/classes.servicemanager.ts', 'adminIdentityGuard'); assertIncludes('cloudly/ts/manager.cluster/classes.clustermanager.ts', 'getClusterById'); assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'installAppCatalogApp'); assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'externalImageRef'); assertIncludes('cloudly/ts/manager.appcatalog/classes.appcatalogmanager.ts', 'SERVICE_DOMAIN'); assertIncludes('cloudly/ts/manager.deployment/classes.deploymentmanager.ts', 'deploymentWorkspaceExec'); assertIncludes('coreflow/ts/coreflow.classes.coreflow.ts', 'deploymentRuntimeManager'); assertIncludes('coreflow/ts/coreflow.classes.deploymentruntime.ts', 'coreflowDeploymentWorkspaceExec'); assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'externalImageRef'); assertIncludes('coreflow/ts/coreflow.classes.clustermanager.ts', 'resolveEnvTemplates'); assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/manifest'"); assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/read'"); assertIncludes('corestore/ts/corestore.classes.corestore.ts', "'/archive/object/write'"); assertIncludes('platformclient/ts/email/classes.emailconnector.ts', 'return response.responseId'); assertIncludes('platformclient/ts/email/classes.letterconnector.ts', 'return response.processId'); assertIncludes('platformclient/ts/email/classes.pushnotificationconnector.ts', "typeof process !== 'undefined'"); assertIncludes('siprouter/ts/sipproxy.ts', 'return { id: callId };'); assertIncludes('siprouter/ts/storage.ts', 'MONGODB_URI'); assertIncludes('siprouter/ts/storage.ts', 'S3_ENDPOINT'); assertIncludes('siprouter/Dockerfile', 'libjpeg-dev'); assertIncludes('siprouter/Dockerfile', 'libtiff-dev'); assertIncludes('siprouter/Dockerfile', 'libjpeg-turbo'); assertIncludes('siprouter/.smartconfig.json', 'linux/amd64'); assertIncludes('siprouter/.smartconfig.json', 'linux/arm64'); assertIncludes('onebox/ts/opsserver/classes.opsserver.ts', "'/v2/*'"); assertIncludes('onebox/ts/classes/appstore.ts', 'installApp(optionsArg: IAppInstallOptions)'); assertIncludes('onebox/ts/classes/appstore.ts', 'publishedPorts: optionsArg.publishedPorts || config.publishedPorts'); assertIncludes('onebox/ts/classes/docker.ts', 'expandPublishedPorts'); assertIncludes('onebox/ts/classes/docker.ts', 'Mounts: this.getSwarmVolumeMounts(service)'); assertIncludes('onebox/ts/database/migrations/migration-016-service-volumes.ts', 'volumes TEXT'); assertIncludes('onebox/ts/database/migrations/migration-017-service-published-ports.ts', 'published_ports TEXT'); assertIncludes('onebox/ts_interfaces/requests/appstore.ts', "method: 'installAppTemplate'"); assertIncludes('onebox/ts_interfaces/requests/appstore.ts', 'publishedPorts?: data.IServicePublishedPort[]'); assertIncludes('onebox/ts/classes/managed-dcrouter.ts', 'DCROUTER_ADMIN_API_TOKEN'); assertIncludes('api/ts/classes.cloudlyapiclient.ts', 'createNodeJumpCommand'); assertIncludes('interfaces/ts/appcatalog/types.ts', 'publishedPorts?: IAppCatalogPublishedPort[]'); assertIncludes('spark/ts/spark.cli.ts', 'CLOUDLY_URL: cloudlyUrl'); assertValidAppCatalog(); const rustdeskConfig = JSON.parse(readWorkspaceFile('appstore-apptemplates/apps/rustdesk-server/versions/1.0.0/config.json')); const relay = rustdeskConfig.envVars.find((entry: { key: string }) => entry.key === 'RELAY'); assert.equal(relay?.value, '${SERVICE_DOMAIN}:21116'); const siprouterConfig = readWorkspaceJson('appstore-apptemplates/apps/siprouter/versions/1.0.0/config.json'); assert.equal(siprouterConfig.platformRequirements?.mongodb, true); assert.equal(siprouterConfig.platformRequirements?.s3, true); assert.ok(siprouterConfig.publishedPorts.some((portArg: any) => portArg.targetPort === 20000 && portArg.targetPortEnd === 20200 && portArg.protocol === 'udp')); const gitopsConfig = readWorkspaceJson('appstore-apptemplates/apps/gitops/versions/1.0.0/config.json'); assert.ok(gitopsConfig.volumes.includes('/data/.serve.zone/gitops')); console.log('codebase regression checks passed');