feat: harden release signing and coverage gate
ForgeFlow quality gate / quality (push) Canceled after 0s
ForgeFlow quality gate / quality (push) Canceled after 0s
This commit is contained in:
@@ -165,3 +165,91 @@ test("profile, review and operation lookups return safe empty values", async (t)
|
||||
await store.deleteInventoryReviewDecision("missing", "workload");
|
||||
await store.deleteDeploymentProfile("missing/repo", "profile");
|
||||
});
|
||||
|
||||
test("session credentials preserve, replace and clear safely when OS encryption is unavailable", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
assert.deepEqual(store.setToken(" session-token "), { persistent: false, preserved: false });
|
||||
assert.equal(store.getToken(), "session-token");
|
||||
assert.deepEqual(store.setToken("", { preserveExisting: true }), { persistent: false, preserved: true });
|
||||
assert.equal(store.getToken(), "session-token");
|
||||
assert.deepEqual(store.setToken("replacement"), { persistent: false, preserved: false });
|
||||
assert.equal(store.getToken(), "replacement");
|
||||
assert.deepEqual(store.setToken(""), { persistent: true, preserved: false });
|
||||
assert.equal(store.getToken(), "");
|
||||
assert.throws(() => store.encryptSecret("password"), (error) => error.code === "SECURE_STORAGE_UNAVAILABLE");
|
||||
assert.equal(store.encryptSecret(""), null);
|
||||
assert.equal(store.decryptSecret(null), "");
|
||||
assert.equal(store.decryptSecret("not-base64-encrypted-data"), "");
|
||||
});
|
||||
|
||||
test("setup, Gitea updates and generic patches retain normalized public state", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
const completed = await store.completeSetup({
|
||||
baseUrl: "https://gitea.test", token: "token", user: { login: "jens" },
|
||||
workspaceRoots: [" C:/Projects ", "C:/Projects", ""]
|
||||
});
|
||||
assert.equal(completed.state.setupComplete, true);
|
||||
assert.equal(completed.state.gitea.hasToken, true);
|
||||
assert.deepEqual(completed.state.workspaceRoots, ["C:/Projects"]);
|
||||
const update = await store.updateGitea({ baseUrl: "https://new.test", token: "", user: null });
|
||||
assert.equal(update.preserved, true);
|
||||
assert.equal(store.data.gitea.user.login, "jens");
|
||||
const patched = await store.patch({ appearance: "light", workspaceRoots: ["D:/Code", "D:/Code"] });
|
||||
assert.equal(patched.appearance, "light");
|
||||
assert.deepEqual(patched.workspaceRoots, ["D:/Code"]);
|
||||
assert.equal("encryptedToken" in patched.gitea, false);
|
||||
});
|
||||
|
||||
test("server saves reject absent credentials before mutating configuration", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
await assert.rejects(
|
||||
store.saveServer({ host: "unraid", username: "root", authType: "password", basePath: "/mnt/apps" }),
|
||||
/password is required/i
|
||||
);
|
||||
await assert.rejects(
|
||||
store.saveServer({ host: "unraid", username: "root", authType: "privateKey", basePath: "/mnt/apps", privateKeyPath: "" }),
|
||||
/select a private key/i
|
||||
);
|
||||
assert.deepEqual(store.data.servers, []);
|
||||
});
|
||||
|
||||
test("deployment profile normalization covers safe defaults and every optional Unraid control", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
const actions = store.normalizeDeploymentProfile({ environment: "qa", statusUrl: "https://app.test/status" });
|
||||
assert.equal(actions.provider, "gitea-actions");
|
||||
assert.equal(actions.name, "qa");
|
||||
assert.equal(actions.branch, "main");
|
||||
assert.equal(actions.workflowFile, "deploy.yml");
|
||||
assert.equal(actions.rollbackWorkflowFile, "");
|
||||
assert.equal(actions.confirmationRequired, true);
|
||||
|
||||
const unraid = store.normalizeDeploymentProfile({
|
||||
id: "all-options", name: " Server ", environment: "production", provider: "ssh-unraid", branch: "release",
|
||||
serverId: " server ", remoteFolder: "apps/App", deploymentMode: "monitor-only", generatedCompose: true,
|
||||
composeFiles: [], composeServices: ["WEB", "Worker"], composeProject: "App.prod", composeWorkingDir: "/mnt/apps/App",
|
||||
containerName: "Visible.App", cloneUrl: "https://gitea.test/Owner/App.git", alignRemote: true,
|
||||
hostPort: -2, containerPort: 70000, webUiUrl: "http://[IP]:[PORT:3000]/", iconMode: "upload",
|
||||
iconFilePath: "C:/icon.png", dockerShell: "/bin/bash", preservePaths: [], adoptedFromServer: true,
|
||||
serverSourceOfTruth: true, manageDockerMan: true, forceRecreate: true, removeOrphans: true,
|
||||
workloadIdentity: { workloadId: "one" }, serverGitAccess: { configured: true, deployKeyId: "invalid", keyFingerprint: "", hostFingerprint: "", configuredAt: "now" },
|
||||
provenance: { remoteFolder: "server" }, detectedMetadata: { source: "docker" }, serverIconReference: " icon ",
|
||||
deploymentPolicy: { frozen: true, freezeReason: " maintenance ", requireNote: true, maintenanceWindows: [{ days: [0, 0, 6, 7, "bad"], start: "01:00", end: "02:00" }] }
|
||||
});
|
||||
assert.equal(unraid.name, "Server");
|
||||
assert.equal(unraid.serverId, "server");
|
||||
assert.equal(unraid.composeFile, "docker-compose.yml");
|
||||
assert.deepEqual(unraid.composeServices, ["web", "worker"]);
|
||||
assert.equal(unraid.hostPort, 1);
|
||||
assert.equal(unraid.containerPort, 65535);
|
||||
assert.equal(unraid.iconMode, "upload");
|
||||
assert.equal(unraid.dockerShell, "/bin/bash");
|
||||
assert.equal(unraid.serverGitAccess.deployKeyId, null);
|
||||
assert.equal(unraid.serverGitAccess.keyFingerprint, null);
|
||||
assert.deepEqual(unraid.deploymentPolicy.maintenanceWindows[0].days, [0, 6]);
|
||||
assert.equal(unraid.serverIconReference, "icon");
|
||||
|
||||
assert.throws(() => store.normalizeDeploymentProfile({ provider: "ssh-unraid", remoteFolder: "app", environment: "prod", composeService: "app", composeServices: ["bad service"] }), /Compose services/);
|
||||
assert.throws(() => store.normalizeDeploymentProfile({ provider: "ssh-unraid", remoteFolder: "app", environment: "prod", composeProject: "bad project!" }), /Compose project/);
|
||||
assert.throws(() => store.normalizeDeploymentProfile({ provider: "ssh-unraid", remoteFolder: "app", environment: "prod", composeWorkingDir: "relative" }), /working directory/);
|
||||
assert.throws(() => store.normalizeDeploymentProfile({ provider: "ssh-unraid", remoteFolder: "app", environment: "prod", containerName: "bad name" }), /Container name/);
|
||||
});
|
||||
|
||||
@@ -63,3 +63,115 @@ test('deployment preflight verifies exact Git, workflow, Actions and server prer
|
||||
assert.equal(result.checks.filter((item) => item.status === 'fail').length, 0);
|
||||
assert.equal(result.head, sha);
|
||||
});
|
||||
|
||||
test('system preflight reports unavailable Git, storage, roots and rejected Gitea credentials', async (t) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-preflight-failures-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const ordinaryFile = path.join(root, 'not-a-directory');
|
||||
await writeFile(ordinaryFile, 'file');
|
||||
const events = [];
|
||||
const service = new PreflightService({
|
||||
store: { data: { gitea: { baseUrl: 'https://stored.test' } }, getToken: () => 'stored-token' },
|
||||
git: { isAvailable: async () => ({ available: false, error: 'git missing' }) },
|
||||
gitea: { validateConnection: async () => { throw new Error('token rejected'); } },
|
||||
deployments: {}, diagnostics: { logDirectory: path.join(root, 'logs'), info: async (...args) => events.push(args) },
|
||||
userDataPath: path.join(root, 'data'), secureStorageAvailable: () => false
|
||||
});
|
||||
service.writableDirectory = async (directory) => {
|
||||
if (directory.endsWith('data')) throw new Error('read only');
|
||||
return true;
|
||||
};
|
||||
const result = await service.runSystem({ roots: [ordinaryFile, path.join(root, 'missing'), ordinaryFile, ''] });
|
||||
assert.equal(result.checks.find((item) => item.id === 'git.available').status, 'fail');
|
||||
assert.equal(result.checks.find((item) => item.id === 'storage.userdata').status, 'fail');
|
||||
assert.equal(result.checks.find((item) => item.id === 'storage.diagnostics').status, 'pass');
|
||||
assert.equal(result.checks.find((item) => item.id === 'storage.credentials').status, 'warning');
|
||||
assert.equal(result.checks.find((item) => item.id === 'workspace.root.0').status, 'fail');
|
||||
assert.equal(result.checks.find((item) => item.id === 'workspace.root.1').status, 'fail');
|
||||
assert.equal(result.checks.find((item) => item.id === 'gitea.connection').status, 'fail');
|
||||
assert.equal(result.summary.ready, false);
|
||||
assert.equal(events[0][0], 'preflight.system.completed');
|
||||
});
|
||||
|
||||
test('system preflight warns on incomplete Git identity and accepts unknown Gitea version', async (t) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-preflight-identity-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const service = new PreflightService({
|
||||
store: { data: { gitea: { baseUrl: '' } }, getToken: () => '' },
|
||||
git: { isAvailable: async () => ({ available: true, version: 'git' }) },
|
||||
gitea: { validateConnection: async () => ({ version: null, user: null, repositoryCount: 0 }) }, deployments: {},
|
||||
diagnostics: { logDirectory: path.join(root, 'logs'), info: async () => {} }, userDataPath: path.join(root, 'data')
|
||||
});
|
||||
service.gitIdentity = async () => ({ name: '', email: '' });
|
||||
const result = await service.runSystem({ baseUrl: 'https://gitea.test', token: 'token', roots: [] });
|
||||
assert.equal(result.checks.find((item) => item.id === 'git.identity').status, 'warning');
|
||||
assert.match(result.checks.find((item) => item.id === 'gitea.connection').detail, /unknown version.*user/i);
|
||||
assert.equal(result.checks.find((item) => item.id === 'gitea.repositories').status, 'pass');
|
||||
assert.equal(result.checks.find((item) => item.id === 'workspace.roots').status, 'warning');
|
||||
|
||||
service.gitIdentity = async () => { throw new Error('identity lookup failed'); };
|
||||
const second = await service.runSystem();
|
||||
assert.match(second.checks.find((item) => item.id === 'git.identity').detail, /lookup failed/i);
|
||||
});
|
||||
|
||||
test('deployment preflight fails fast for invalid identity, profile and missing local link', async () => {
|
||||
const diagnostics = [];
|
||||
const service = new PreflightService({
|
||||
store: { getDeploymentProfile: (_name, id) => id === 'known' ? { id: 'known', name: 'Production' } : null },
|
||||
git: {}, gitea: {}, deployments: {}, diagnostics: { info: async (...args) => diagnostics.push(args) }, userDataPath: ''
|
||||
});
|
||||
await assert.rejects(service.runDeployment({ repository: null, profileId: 'known' }), /identity is required/i);
|
||||
await assert.rejects(service.runDeployment({ repository: { fullName: 'owner/app' }, profileId: 'missing' }), /profile not found/i);
|
||||
const result = await service.runDeployment({ repository: { fullName: 'owner/app', localPath: '' }, profileId: 'known' });
|
||||
assert.deepEqual(result.summary.blocking, ['repository.linked']);
|
||||
assert.equal(diagnostics[0][0], 'preflight.deployment.completed');
|
||||
});
|
||||
|
||||
test('deployment preflight preserves actionable evidence across Git, workflow and endpoint failures', async (t) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-preflight-degraded-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const profile = { id: 'production', name: 'Production', environment: 'production', branch: 'main', workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml' };
|
||||
const service = new PreflightService({
|
||||
store: { getDeploymentProfile: () => profile },
|
||||
git: {
|
||||
status: async () => ({ root, head: 'b'.repeat(40), clean: false, counts: { changed: 4 }, branch: { head: '', upstream: '', ahead: 2, behind: 3 } }),
|
||||
verifyCommitOnRemoteBranch: async () => { throw new Error('commit not published'); }
|
||||
},
|
||||
gitea: { repositoryFileExists: async () => false, listWorkflowRuns: async () => { throw new Error('Actions disabled'); } },
|
||||
deployments: {}, diagnostics: { info: async () => {} }, userDataPath: root
|
||||
});
|
||||
const result = await service.runDeployment({ repository: { fullName: 'owner/app', localPath: root }, profileId: profile.id });
|
||||
for (const id of ['git.branch', 'git.clean', 'git.upstream', 'git.sync', 'git.remote-sha', 'workflow.deploy.local', 'workflow.deploy.remote', 'gitea.actions', 'server.status.configured']) {
|
||||
assert.equal(result.checks.find((item) => item.id === id).status, 'fail', id);
|
||||
}
|
||||
assert.equal(result.checks.find((item) => item.id === 'workflow.rollback.local').status, 'warning');
|
||||
assert.equal(result.checks.find((item) => item.id === 'server.health').status, 'warning');
|
||||
assert.equal(result.head, 'b'.repeat(40));
|
||||
});
|
||||
|
||||
test('deployment preflight distinguishes unreachable and mismatched status evidence', async (t) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-preflight-status-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
await mkdir(path.join(root, '.gitea', 'workflows'), { recursive: true });
|
||||
await writeFile(path.join(root, '.gitea', 'workflows', 'deploy.yml'), 'name: deploy\n');
|
||||
const profile = { id: 'production', name: 'Production', environment: 'production', branch: 'main', workflowFile: 'deploy.yml', statusUrl: 'https://app/status', healthcheckUrl: 'https://app/health' };
|
||||
let status = { reachable: false, ok: false, status: 503, error: '' };
|
||||
const service = new PreflightService({
|
||||
store: { getDeploymentProfile: () => profile },
|
||||
git: { status: async () => { throw new Error('checkout corrupt'); } },
|
||||
gitea: { repositoryFileExists: async () => { throw new Error('Gitea offline'); } },
|
||||
deployments: { readStatusEndpoint: async () => status, checkHealth: async () => ({ healthy: false, status: 500, error: '' }) },
|
||||
diagnostics: { info: async () => {} }, userDataPath: root
|
||||
});
|
||||
const unreachable = await service.runDeployment({ repository: { fullName: 'owner/app', localPath: root }, profileId: profile.id });
|
||||
assert.match(unreachable.checks.find((item) => item.id === 'server.status.reachable').detail, /HTTP 503/i);
|
||||
assert.match(unreachable.checks.find((item) => item.id === 'server.health').detail, /HTTP 500/i);
|
||||
assert.match(unreachable.checks.find((item) => item.id === 'git.repository').detail, /checkout corrupt/i);
|
||||
|
||||
status = { reachable: true, ok: true, repository: 'other/app', environment: 'staging', liveSha: null };
|
||||
const mismatch = await service.runDeployment({ repository: { fullName: 'owner/app', localPath: root }, profileId: profile.id });
|
||||
const identity = mismatch.checks.find((item) => item.id === 'server.status.identity');
|
||||
assert.equal(identity.status, 'fail');
|
||||
assert.equal(identity.required, true);
|
||||
assert.match(mismatch.checks.find((item) => item.id === 'server.status.reachable').detail, /no live SHA/i);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
parseServerInventory, buildWorkloadInventory, inventoryContainerMatch, remoteIdentity,
|
||||
stableWorkloadId, profileMatchesWorkload, sanitizeLegacyContainer, safeRelativeToBase,
|
||||
canonicalServerAppdataPath, deploymentRootCandidate,
|
||||
} = require('../src/main/server-inventory.cjs');
|
||||
|
||||
const b64 = (value) => Buffer.from(String(value)).toString('base64');
|
||||
|
||||
test('inventory parser handles every evidence record and ignores malformed payloads', () => {
|
||||
const legacy = { Id: 'legacy', Name: '/App', Config: { Image: 'app:1', Labels: { 'com.docker.compose.project': 'app' } }, State: { Running: true, Status: 'running', Health: { Status: 'healthy' } }, Mounts: null };
|
||||
const safe = { id: 'safe', name: '/Safe', running: false, labels: null, mounts: null, ports: null, networks: null };
|
||||
const projects = [{ Name: 'app', Status: 'running(1)', ConfigFiles: '/mnt/user/appdata/App/compose.yml,/mnt/user/appdata/App/extra.yml' }, { name: '', configFiles: [] }];
|
||||
const output = [
|
||||
'noise', '__FORGEFLOW_INVENTORY__',
|
||||
`H\ttrue\tfalse\ttrue\ttrue\tfalse\ttrue\t${b64('Compose v2')}\t${b64('Linux')}`,
|
||||
`R\t${b64('/mnt/user/appdata/App')}\t${b64('git@gitea.test:Owner/App.git')}\t${'a'.repeat(40)}\t${b64('main')}`,
|
||||
`C\t${b64(JSON.stringify([legacy, null]))}`,
|
||||
`C\t${b64(JSON.stringify(safe))}`,
|
||||
`C\t${b64('{bad json')}`,
|
||||
`D\t${b64('App')}\t${b64('/templates/App.xml')}\t${b64('http://app')}\t${b64('/icon.png')}\t${b64('/bin/bash')}\t${b64('app:1')}\t${b64('bridge')}`,
|
||||
`P\t${b64(JSON.stringify(projects))}`,
|
||||
`P\t${b64(JSON.stringify({ name: 'single', status: 'exited', config_files: ['single.yml'] }))}`,
|
||||
`Y\t${b64('/mnt/user/appdata/App/')}\t${b64('compose.yml\ncompose.prod.yml\n')}\t${b64('app')}\t${b64('web\nworker')}\t${b64('app:1')}\ttrue\t${b64('')}`,
|
||||
`W\t${b64('partial docker inspect failure')}`,
|
||||
'UNKNOWN\tignored',
|
||||
].join('\n');
|
||||
const parsed = parseServerInventory(output);
|
||||
assert.deepEqual(parsed.capabilities, { docker: true, compose: false, git: true, tar: true, checksum: false, baseWritable: true, composeVersion: 'Compose v2', platform: 'Linux' });
|
||||
assert.equal(parsed.checkouts.length, 1);
|
||||
assert.equal(parsed.containers.length, 2);
|
||||
assert.equal(parsed.containers[0].health, 'healthy');
|
||||
assert.deepEqual(parsed.containers[1].labels, {});
|
||||
assert.equal(parsed.dockerMan[0].templatePath, '/templates/App.xml');
|
||||
assert.equal(parsed.composeProjects.length, 2);
|
||||
assert.deepEqual(parsed.composeProjects[0].configFiles, ['/mnt/user/appdata/App/compose.yml', '/mnt/user/appdata/App/extra.yml']);
|
||||
assert.deepEqual(parsed.composeDefinitions[0].services, ['web', 'worker']);
|
||||
assert.deepEqual(parsed.warnings, ['partial docker inspect failure']);
|
||||
assert.throws(() => parseServerInventory('ordinary output'), /did not return/i);
|
||||
});
|
||||
|
||||
test('server path normalization keeps deployments inside canonical appdata', () => {
|
||||
assert.equal(safeRelativeToBase('/mnt/user/appdata/', '/mnt/user/appdata/App/'), 'App');
|
||||
for (const value of ['', '/mnt/user/appdata', '/mnt/user/appdata/../etc', '/other/App']) assert.equal(safeRelativeToBase('/mnt/user/appdata', value), '');
|
||||
assert.equal(canonicalServerAppdataPath('/mnt/user/appdata', '/mnt/cache/appdata/App'), '/mnt/user/appdata/App');
|
||||
assert.equal(canonicalServerAppdataPath('/mnt/user/appdata', '/mnt/disk2/appdata/App/data'), '/mnt/user/appdata/App/data');
|
||||
assert.equal(canonicalServerAppdataPath('', '/mnt/user/appdata/App'), '/mnt/user/appdata/App');
|
||||
assert.equal(canonicalServerAppdataPath('/custom', '/outside/path'), '/outside/path');
|
||||
assert.equal(canonicalServerAppdataPath('/custom', ''), '');
|
||||
assert.equal(deploymentRootCandidate('App/source-pre-abcdef1/source'), 'App');
|
||||
assert.equal(deploymentRootCandidate('App/.forgeflow/incoming'), 'App');
|
||||
});
|
||||
|
||||
test('profile matching requires the same server and accepts each stable identity form', () => {
|
||||
const workload = { serverId: 'server', workloadId: 'workload', selector: { kind: 'compose', composeProject: 'app' }, compose: { project: 'app', workingDir: '/apps/App' }, remoteFolderCandidate: 'App', containers: [{ name: 'app-web' }] };
|
||||
assert.equal(profileMatchesWorkload(null, workload), false);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'gitea-actions', serverId: 'server' }, workload), false);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'other' }, workload), false);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', workloadIdentity: { workloadId: 'workload' } }, workload), true);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', workloadIdentity: { selector: workload.selector } }, workload), true);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', composeProject: 'app' }, workload), true);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', composeProject: 'app', composeWorkingDir: '/other' }, workload), false);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', remoteFolder: 'App' }, workload), true);
|
||||
assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', containerName: 'app-web' }, workload), true);
|
||||
assert.equal(stableWorkloadId('server', workload.selector), stableWorkloadId('server', workload.selector));
|
||||
});
|
||||
|
||||
test('container matching prioritizes working directory, mounts, provenance and stable names', () => {
|
||||
const checkout = { root: '/apps/App', remote: 'git@gitea.test:Owner/App.git' };
|
||||
const repository = { name: 'App' };
|
||||
const base = { running: true, labels: {}, mounts: [], name: 'different' };
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'com.docker.compose.project.working_dir': '/apps/App/' } }), 100);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, mounts: [{ Source: '/apps/App/data' }] }), 90);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'org.opencontainers.image.source': 'https://gitea.test/Owner/App' } }), 85);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'com.docker.compose.project': 'app' } }), 70);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, name: '/APP' }), 60);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, { ...base, running: false }), 0);
|
||||
assert.equal(inventoryContainerMatch(checkout, repository, base), 0);
|
||||
assert.equal(remoteIdentity(''), '');
|
||||
});
|
||||
|
||||
test('workload builder merges runtime, Compose file and DockerMan evidence without backups', () => {
|
||||
const labels = {
|
||||
'com.docker.compose.project': 'app',
|
||||
'com.docker.compose.project.working_dir': '/mnt/cache/appdata/App',
|
||||
'com.docker.compose.project.config_files': '/mnt/cache/appdata/App/compose.yml',
|
||||
'com.docker.compose.service': 'web',
|
||||
'tech.itworx.forgeflow.repository': 'git@gitea.test:Owner/App.git',
|
||||
'tech.itworx.forgeflow.commit': 'b'.repeat(40),
|
||||
'tech.itworx.forgeflow.branch': 'main',
|
||||
};
|
||||
const inventory = {
|
||||
containers: [
|
||||
{ id: 'web', name: 'app-web', image: 'registry/app:1', imageId: 'image', running: true, status: 'running', health: 'unhealthy', labels, ports: { '8080/tcp': null, '3000/udp': [{ HostIp: '0.0.0.0', HostPort: '3000' }] }, mounts: [{ Type: 'bind', Source: '/mnt/disk1/appdata/App/data', Destination: '/data', RW: false }], networks: { frontend: {} }, restartPolicy: 'always' },
|
||||
{ id: 'worker', name: 'app-worker', image: 'registry/worker:1', imageId: 'worker', running: false, status: 'exited', health: null, labels: { ...labels, 'com.docker.compose.service': 'worker' }, ports: {}, mounts: [], networks: {}, restartPolicy: '' },
|
||||
],
|
||||
checkouts: [{ root: '/mnt/user/appdata/App', remote: 'git@gitea.test:Owner/App.git', liveSha: 'c'.repeat(40), branch: 'release' }],
|
||||
composeProjects: [{ name: 'app', status: 'running', configFiles: [] }, { name: 'headless', status: 'exited', configFiles: ['/mnt/user/appdata/Headless/compose.yml'] }],
|
||||
composeDefinitions: [
|
||||
{ workingDir: '/mnt/user/appdata/App', configFiles: ['/mnt/user/appdata/App/compose.yml'], projectName: 'app', services: ['web', 'worker'], images: ['registry/app:1'], valid: true, error: '' },
|
||||
{ workingDir: '/mnt/user/appdata/Backup/.forgeflow/releases/one', configFiles: ['compose.yml'], projectName: 'backup', services: [], images: [], valid: true },
|
||||
{ workingDir: '/mnt/user/appdata/Standalone', configFiles: ['/mnt/user/appdata/Standalone/compose.yml'], projectName: '', services: ['api'], images: ['standalone:1'], valid: false, error: 'invalid compose' },
|
||||
],
|
||||
dockerMan: [
|
||||
{ name: 'app-web', templatePath: '/templates/app.xml', webUiUrl: 'http://app', iconUrl: '/app.png', shell: '/bin/bash', repository: 'registry/app:1', network: 'frontend' },
|
||||
{ name: 'template-only', templatePath: '/templates/template.xml', webUiUrl: '', iconUrl: '', shell: '', repository: 'template:1', network: 'bridge' },
|
||||
], warnings: [], capabilities: {},
|
||||
};
|
||||
const repository = { fullName: 'Owner/App', name: 'App', cloneUrl: 'https://gitea.test/Owner/App.git' };
|
||||
const workloads = buildWorkloadInventory({ inventory, server: { id: 'server', name: 'Unraid', basePath: '/mnt/user/appdata' }, repositories: [repository], profiles: [{ id: 'profile', provider: 'ssh-unraid', serverId: 'server', composeProject: 'app', repositoryFullName: 'Owner/App', adoptedFromServer: true }] });
|
||||
assert.equal(workloads.some((workload) => workload.displayName === 'backup'), false);
|
||||
const app = workloads.find((workload) => workload.displayName === 'app');
|
||||
assert.equal(app.status, 'linked');
|
||||
assert.equal(app.runtime.running, true);
|
||||
assert.equal(app.runtime.allRunning, false);
|
||||
assert.equal(app.runtime.health, 'unhealthy');
|
||||
assert.equal(app.runtime.ports.length, 2);
|
||||
assert.equal(app.containers[0].mounts[0].readOnly, true);
|
||||
assert.equal(app.remoteFolderCandidate, 'App');
|
||||
assert.equal(app.candidates[0].exact, true);
|
||||
assert.equal(app.link.source, 'automatic');
|
||||
const standalone = workloads.find((workload) => workload.displayName === 'Standalone');
|
||||
assert.equal(standalone.metadata.composeDefinitionValid, false);
|
||||
assert.equal(standalone.metadata.composeDefinitionError, 'invalid compose');
|
||||
const template = workloads.find((workload) => workload.displayName === 'template-only');
|
||||
assert.equal(template.kind, 'dockerman-container');
|
||||
assert.equal(template.runtime.running, false);
|
||||
assert.equal(template.metadata.shell, '/bin/sh');
|
||||
});
|
||||
|
||||
test('legacy container sanitizer applies safe defaults to partial Docker inspect data', () => {
|
||||
assert.deepEqual(sanitizeLegacyContainer(null), {
|
||||
id: '', name: '', image: '', imageId: '', running: false, status: '', health: null,
|
||||
labels: {
|
||||
'com.docker.compose.project': '', 'com.docker.compose.project.working_dir': '', 'com.docker.compose.project.config_files': '', 'com.docker.compose.service': '',
|
||||
'org.opencontainers.image.source': '', 'org.opencontainers.image.revision': '', 'tech.itworx.forgeflow.repository': '', 'tech.itworx.forgeflow.commit': '',
|
||||
'tech.itworx.forgeflow.branch': '', 'net.unraid.docker.webui': '', 'net.unraid.docker.icon': '', 'net.unraid.docker.shell': '', 'net.unraid.docker.managed': '',
|
||||
}, ports: {}, mounts: [], networks: {}, restartPolicy: '',
|
||||
});
|
||||
});
|
||||
@@ -528,6 +528,22 @@ test("Windows release pipeline fails closed on signatures and emits provenance p
|
||||
assert.match(publisher, /sbom\.cdx\.json/);
|
||||
});
|
||||
|
||||
test("production signing build supports classic and Azure identities but always fails closed", async () => {
|
||||
const [pkg, validator, signedConfig] = await Promise.all([
|
||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/validate-signing-environment.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/signed-electron-builder-config.cjs", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(pkg, /dist:win:signed/);
|
||||
assert.match(validator, /FORGEFLOW_SIGNED_RELEASE/);
|
||||
assert.match(validator, /WIN_CSC_LINK/);
|
||||
assert.match(validator, /FORGEFLOW_AZURE_CERTIFICATE_PROFILE/);
|
||||
assert.match(validator, /exact certificate subject/);
|
||||
assert.match(signedConfig, /forceCodeSigning:\s*true/);
|
||||
assert.match(signedConfig, /azureSignOptions/);
|
||||
assert.match(signedConfig, /timestamp\.acs\.microsoft\.com/);
|
||||
});
|
||||
|
||||
test("binary update helper verifies, waits, applies and records restart state", async () => {
|
||||
const helper = await readFile(
|
||||
new URL("../scripts/apply-binary-update.ps1", import.meta.url),
|
||||
|
||||
Reference in New Issue
Block a user