Publish curated ForgeFlow source from 2ed1787c0b52
ForgeFlow quality gate / quality (push) Successful in 4m12s
ForgeFlow quality gate / secret-scan (push) Successful in 7s

This commit is contained in:
NuklearRabbit committed 2026-09-29 22:50:57 +02:00
commit f60b269686
254 files changed
+46204

No files matched your search

+497
View File
@@ -0,0 +1,497 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import deploymentModule from '../src/main/deployment-service.cjs';
const { DeploymentService } = deploymentModule;
const SHA = 'a'.repeat(40);
const PREVIOUS_SHA = 'b'.repeat(40);
async function serve(handler) {
const server = http.createServer(handler);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
return {
url: `http://127.0.0.1:${server.address().port}/status`,
close: () => new Promise((resolve) => server.close(resolve))
};
}
function jsonEndpoint(body, statusCode = 200) {
return serve((request, response) => {
response.writeHead(statusCode, { 'Content-Type': 'application/json' });
response.end(typeof body === 'string' ? body : JSON.stringify(body));
});
}
// A port nothing listens on, so the request fails instead of hanging.
async function unreachableUrl() {
const closed = await serve(() => {});
await closed.close();
return closed.url;
}
function makeStore({ profile = null, operations = [] } = {}) {
const saved = new Map(operations.map((item) => [item.id, item]));
const states = new Map();
return {
data: { operations, gitea: { baseUrl: 'https://gitea.example' } },
getToken: () => 'gitea-secret-token',
getDeploymentProfile: () => profile,
getOperation: (id) => saved.get(id) || null,
addOperation: async (operation) => {
saved.set(operation.id, structuredClone(operation));
return structuredClone(operation);
},
saveDeploymentState: async (profileId, state) => {
states.set(profileId, state);
return state;
},
saved,
states
};
}
function makeOperation(overrides = {}) {
return {
id: 'operation-1',
type: 'deployment',
action: 'deploy',
status: 'queued',
repository: 'jens/app',
profileId: 'production',
environment: 'production',
workflowFile: 'deploy.yml',
branch: 'main',
sha: SHA,
shortSha: SHA.slice(0, 7),
dispatchedAt: new Date().toISOString(),
stages: new DeploymentService({}, {}, {}).makeStages(),
logs: [],
...overrides
};
}
function successPayload(overrides = {}) {
return {
repository: 'jens/app',
environment: 'production',
commit_sha: SHA,
previous_sha: PREVIOUS_SHA,
requested_sha: SHA,
request_id: 'operation-1',
last_exit_code: 0,
health: 'healthy',
...overrides
};
}
test('the status endpoint reader accepts both key spellings and refuses anything that is not a commit SHA', async (context) => {
const service = new DeploymentService(makeStore(), {}, {});
assert.deepEqual(await service.readStatusEndpoint(''), { configured: false });
const snake = await jsonEndpoint(successPayload());
context.after(() => snake.close());
const snakeResult = await service.readStatusEndpoint(snake.url);
assert.equal(snakeResult.ok, true);
assert.equal(snakeResult.liveSha, SHA);
assert.equal(snakeResult.previousSha, PREVIOUS_SHA);
assert.equal(snakeResult.requestedSha, SHA);
assert.equal(snakeResult.requestId, 'operation-1');
assert.equal(snakeResult.lastExitCode, 0);
const camel = await jsonEndpoint({
repository: 'jens/app',
environment: 'PRODUCTION',
commitSha: SHA.toUpperCase(),
previousSha: PREVIOUS_SHA,
requestedSha: SHA,
requestId: 'operation-1',
lastExitCode: 3
});
context.after(() => camel.close());
const camelResult = await service.readStatusEndpoint(camel.url);
assert.equal(camelResult.liveSha, SHA, 'a SHA is normalised to lower case');
assert.equal(camelResult.environment, 'production', 'the environment is compared case-insensitively');
assert.equal(camelResult.lastExitCode, 3);
const untrusted = await jsonEndpoint({ commit_sha: 'HEAD', previous_sha: 'v1.2.3', request_id: 42, requested_sha: 'not-a-sha' });
context.after(() => untrusted.close());
const untrustedResult = await service.readStatusEndpoint(untrusted.url);
assert.equal(untrustedResult.liveSha, null);
assert.equal(untrustedResult.previousSha, null);
assert.equal(untrustedResult.requestedSha, null);
assert.equal(untrustedResult.requestId, null, 'a non-string request id is not accepted');
});
test('an unreachable or failing status endpoint is reported instead of assumed healthy', async (context) => {
const service = new DeploymentService(makeStore(), {}, {});
const failing = await jsonEndpoint({ error: 'boom' }, 503);
context.after(() => failing.close());
const failed = await service.readStatusEndpoint(failing.url);
assert.deepEqual(
{ configured: failed.configured, reachable: failed.reachable, ok: failed.ok, status: failed.status },
{ configured: true, reachable: true, ok: false, status: 503 }
);
const offline = await service.readStatusEndpoint(await unreachableUrl());
assert.equal(offline.reachable, false);
assert.equal(offline.ok, false);
assert.ok(offline.error);
});
test('healthchecks distinguish unconfigured, healthy, rejected and unreachable', async (context) => {
const service = new DeploymentService(makeStore(), {}, {});
assert.deepEqual(await service.checkHealth(''), { configured: false, healthy: null });
const healthy = await jsonEndpoint({ ok: true });
context.after(() => healthy.close());
const healthyResult = await service.checkHealth(healthy.url);
assert.equal(healthyResult.healthy, true);
assert.equal(healthyResult.status, 200);
const rejected = await jsonEndpoint({ ok: false }, 500);
context.after(() => rejected.close());
assert.equal((await service.checkHealth(rejected.url)).healthy, false);
const offline = await service.checkHealth(await unreachableUrl());
assert.equal(offline.healthy, false);
assert.ok(offline.error);
});
test('profile state derives health from the status document when no healthcheck is configured', async (context) => {
const endpoint = await jsonEndpoint(successPayload({ health: 'degraded', deployed_at: '2026-08-01T10:00:00.000Z' }));
context.after(() => endpoint.close());
const profile = { id: 'production', environment: 'production', statusUrl: endpoint.url, healthcheckUrl: '' };
const store = makeStore({ profile });
const service = new DeploymentService(store, {}, {});
const state = await service.refreshProfileState('jens/app', 'production', { expectedSha: SHA });
assert.equal(state.healthConfigured, false);
assert.equal(state.healthy, false, 'a degraded status document is not treated as healthy');
assert.equal(state.liveSha, SHA);
assert.equal(state.versionMatches, true);
assert.equal(state.deployedAt, '2026-08-01T10:00:00.000Z');
assert.equal(store.states.get('production').liveSha, SHA, 'the state is persisted');
});
test('an unknown health word leaves the health state undecided rather than guessing', async (context) => {
const endpoint = await jsonEndpoint(successPayload({ health: 'starting' }));
context.after(() => endpoint.close());
const store = makeStore({ profile: { id: 'production', environment: 'production', statusUrl: endpoint.url, healthcheckUrl: '' } });
const state = await new DeploymentService(store, {}, {}).refreshProfileState('jens/app', 'production');
assert.equal(state.healthy, null);
assert.equal(state.versionMatches, null, 'without an expected SHA there is nothing to compare');
});
test('refreshing the state of a removed profile fails loudly', async () => {
const service = new DeploymentService(makeStore({ profile: null }), {}, {});
await assert.rejects(() => service.refreshProfileState('jens/app', 'gone'), /Deployment profile not found/);
});
test('a terminal operation is never polled again', async () => {
const operation = makeOperation({ status: 'success' });
const store = makeStore({ operations: [operation] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => assert.fail('a finished deployment must not be polled'),
listWorkflowJobs: async () => assert.fail('a finished deployment must not be polled')
}, {});
assert.equal((await service.refreshOperation('operation-1')).status, 'success');
});
test('an unknown operation is reported instead of silently ignored', async () => {
const service = new DeploymentService(makeStore(), {}, {});
await assert.rejects(() => service.refreshOperation('missing'), /Deployment operation not found/);
});
test('a workflow run that is not visible yet keeps the deployment queued', async () => {
const store = makeStore({ profile: { id: 'production' }, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => ({ run: null, source: 'actions' })
}, {});
const refreshed = await service.refreshOperation('operation-1');
assert.equal(refreshed.status, 'queued');
assert.equal(refreshed.stages.find((stage) => stage.id === 'queued').status, 'active');
assert.match(refreshed.logs.at(-1), /queued or not visible/);
});
test('a failed runner marks the deployment failed and skips verification', async () => {
const store = makeStore({ profile: { id: 'production' }, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => ({ run: { id: 7, runNumber: 7, status: 'completed', conclusion: 'failure', htmlUrl: 'https://gitea.example/run/7' }, source: 'actions' }),
listWorkflowJobs: async () => [{ name: 'build', status: 'completed', conclusion: 'failure' }]
}, {});
const refreshed = await service.refreshOperation('operation-1');
assert.equal(refreshed.status, 'failed');
assert.equal(refreshed.failure.stage, 'runner');
assert.equal(refreshed.stages.find((stage) => stage.id === 'healthcheck').status, 'skipped');
assert.equal(refreshed.runUrl, 'https://gitea.example/run/7');
});
test('a successful runner still fails when the server does not prove it runs the exact commit', async (context) => {
const endpoint = await jsonEndpoint(successPayload({ commit_sha: 'c'.repeat(40) }));
context.after(() => endpoint.close());
const profile = { id: 'production', environment: 'production', statusUrl: endpoint.url, healthcheckUrl: '' };
const store = makeStore({ profile, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => ({ run: { id: 8, runNumber: 8, status: 'completed', conclusion: 'success' }, source: 'actions' }),
listWorkflowJobs: async () => []
}, {});
const refreshed = await service.refreshOperation('operation-1');
assert.equal(refreshed.status, 'failed');
assert.equal(refreshed.failure.stage, 'version-verification');
assert.match(refreshed.failure.message, /instead of/);
assert.equal(refreshed.stages.find((stage) => stage.id === 'complete').status, 'failed');
});
test('a verified deployment completes, and the same evidence marks a rollback as rolled back', async (context) => {
const endpoint = await jsonEndpoint(successPayload());
context.after(() => endpoint.close());
const profile = { id: 'production', environment: 'production', statusUrl: endpoint.url, healthcheckUrl: '' };
const gitea = {
findWorkflowRun: async () => ({ run: { id: 9, runNumber: 9, status: 'completed', conclusion: 'success' }, source: 'actions' }),
listWorkflowJobs: async () => [{ name: 'deploy', status: 'completed', conclusion: 'success' }]
};
const deployStore = makeStore({ profile, operations: [makeOperation()] });
const deployed = await new DeploymentService(deployStore, gitea, {}).refreshOperation('operation-1');
assert.equal(deployed.status, 'success');
assert.equal(deployed.stages.find((stage) => stage.id === 'complete').status, 'complete');
assert.equal(deployed.applicationState.liveSha, SHA);
assert.ok(deployed.logs.some((line) => line.includes('[job] deploy: success')));
const rollbackStore = makeStore({ profile, operations: [makeOperation({ action: 'rollback' })] });
const rolledBack = await new DeploymentService(rollbackStore, gitea, {}).refreshOperation('operation-1');
assert.equal(rolledBack.status, 'rolled-back');
});
test('unavailable job details degrade to a warning instead of failing the refresh', async () => {
const store = makeStore({ profile: { id: 'production' }, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => ({ run: { id: 10, runNumber: 10, status: 'in_progress', conclusion: null }, source: 'actions' }),
listWorkflowJobs: async () => { throw new Error('jobs API disabled'); }
}, {});
const refreshed = await service.refreshOperation('operation-1');
assert.equal(refreshed.status, 'running');
assert.equal(refreshed.stages.find((stage) => stage.id === 'runner').status, 'active');
assert.ok(refreshed.logs.some((line) => line.includes('Job details unavailable: jobs API disabled')));
});
test('a failing poll is recorded on the operation without losing it', async () => {
const store = makeStore({ profile: { id: 'production' }, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => { throw new Error('Gitea unreachable'); }
}, {});
const refreshed = await service.refreshOperation('operation-1');
assert.equal(refreshed.pollError, 'Gitea unreachable');
assert.equal(refreshed.status, 'queued', 'the operation keeps its last known state');
assert.ok(refreshed.logs.some((line) => line.includes('Status refresh failed')));
});
test('a deployment whose profile was deleted reports that instead of crashing the poll', async () => {
const store = makeStore({ profile: null, operations: [makeOperation()] });
const refreshed = await new DeploymentService(store, {}, {}).refreshOperation('operation-1');
assert.match(refreshed.pollError, /profile used by this operation no longer exists/);
});
test('a refresh already in flight is not started a second time', async () => {
let calls = 0;
let release;
const gate = new Promise((resolve) => { release = resolve; });
const store = makeStore({ profile: { id: 'production' }, operations: [makeOperation()] });
const service = new DeploymentService(store, {
findWorkflowRun: async () => { calls += 1; await gate; return { run: null, source: 'actions' }; }
}, {});
const first = service.refreshOperation('operation-1');
const second = await service.refreshOperation('operation-1');
assert.equal(second.status, 'queued');
release();
await first;
assert.equal(calls, 1, 'the second caller reuses the in-flight refresh');
});
test('job states drive the runner stage', () => {
const service = new DeploymentService(makeStore(), {}, {});
const stageOf = (jobs) => {
const operation = makeOperation();
service.mapJobsToStages(operation, jobs);
return operation.stages.find((stage) => stage.id === 'runner').status;
};
assert.equal(stageOf([{ status: 'in_progress' }]), 'active');
assert.equal(stageOf([{ conclusion: 'success' }, { conclusion: 'failure' }]), 'failed');
assert.equal(stageOf([{ conclusion: 'success' }]), 'complete');
assert.equal(stageOf([{ status: 'waiting' }]), 'pending');
const untouched = makeOperation();
service.mapJobsToStages(untouched, []);
assert.equal(untouched.stages.find((stage) => stage.id === 'queued').status, 'active', 'no jobs leaves the stages alone');
});
test('a rejected dispatch records the failure on the operation and still surfaces the error', async () => {
const profile = {
id: 'production', name: 'Production', environment: 'production', branch: 'main',
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml',
statusUrl: 'https://app.example.test/.well-known/forgeflow'
};
const store = makeStore({ profile });
const service = new DeploymentService(store, {
listWorkflowRuns: async () => ({ runs: [{ id: 1 }, { id: 2 }] }),
dispatchWorkflow: async () => { throw new Error('workflow file not found'); }
}, {
status: async () => ({ head: SHA, clean: true, counts: { changed: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } }),
verifyCommitOnRemoteBranch: async () => ({ valid: true })
}, { info: async () => {}, error: async () => {} });
await assert.rejects(
() => service.deploy({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: 'production', sha: SHA }),
/workflow file not found/
);
const stored = [...store.saved.values()].at(-1);
assert.equal(stored.status, 'failed');
assert.equal(stored.failure.stage, 'dispatch');
assert.deepEqual(stored.baselineRunIds, ['1', '2'], 'runs that existed before dispatch are never mistaken for this one');
assert.equal(stored.stages.find((stage) => stage.id === 'queued').status, 'failed');
});
test('a rejected rollback dispatch is recorded the same way as a rejected deployment', async (context) => {
const endpoint = await jsonEndpoint(successPayload());
context.after(() => endpoint.close());
const profile = {
id: 'production', name: 'Production', environment: 'production', branch: 'main',
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml', statusUrl: endpoint.url, healthcheckUrl: ''
};
const store = makeStore({ profile });
const service = new DeploymentService(store, {
listWorkflowRuns: async () => ({ runs: [] }),
dispatchWorkflow: async () => { throw new Error('rollback workflow is disabled'); }
}, {
verifyCommitOnRemoteBranch: async () => ({ valid: true })
}, { info: async () => {}, error: async () => {} });
await assert.rejects(
() => service.rollback({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: 'production', targetSha: PREVIOUS_SHA }),
/rollback workflow is disabled/
);
const stored = [...store.saved.values()].at(-1);
assert.equal(stored.action, 'rollback');
assert.equal(stored.status, 'failed');
assert.equal(stored.failure.stage, 'dispatch');
assert.equal(stored.workflowFile, 'rollback.yml');
});
test('rollback refuses every state where the target is not the server-reported previous version', async (context) => {
const endpoint = await jsonEndpoint(successPayload());
context.after(() => endpoint.close());
const base = {
id: 'production', name: 'Production', environment: 'production', branch: 'main',
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml', statusUrl: endpoint.url, healthcheckUrl: ''
};
const git = { verifyCommitOnRemoteBranch: async () => ({ valid: true }) };
const repository = { fullName: 'jens/app', localPath: '/repo' };
const rollback = (profile, targetSha) => new DeploymentService(makeStore({ profile }), {}, git)
.rollback({ repository, profileId: 'production', targetSha });
await assert.rejects(() => rollback({ ...base, rollbackWorkflowFile: '' }, PREVIOUS_SHA), /No rollback workflow is configured/);
await assert.rejects(() => rollback(base, 'c'.repeat(40)), /no longer the previous server version/);
await assert.rejects(() => rollback(base, SHA), /no longer the previous server version/, 'the live commit is not the previous one either');
// The "already live" guard only remains reachable when the server reports the
// same commit as both its live and its previous version.
const stuck = await jsonEndpoint(successPayload({ previous_sha: SHA }));
context.after(() => stuck.close());
await assert.rejects(() => rollback({ ...base, statusUrl: stuck.url }, SHA), /already live/);
const noPrevious = await jsonEndpoint(successPayload({ previous_sha: null }));
context.after(() => noPrevious.close());
await assert.rejects(() => rollback({ ...base, statusUrl: noPrevious.url }, PREVIOUS_SHA), /does not report a previous version/);
const otherEnvironment = await jsonEndpoint(successPayload({ environment: 'staging' }));
context.after(() => otherEnvironment.close());
await assert.rejects(() => rollback({ ...base, statusUrl: otherEnvironment.url }, PREVIOUS_SHA), /does not match this repository and environment/);
// An unreachable endpoint surfaces the underlying network error rather than a
// generic message, so the reason a rollback was refused stays diagnosable.
const unreachable = { ...base, statusUrl: await unreachableUrl() };
await assert.rejects(() => rollback(unreachable, PREVIOUS_SHA), /fetch failed|ECONNREFUSED|must be reachable/i);
});
test('an unavailable run baseline degrades to a warning rather than blocking the dispatch', async () => {
const profile = {
id: 'production', name: 'Production', environment: 'production', branch: 'main',
workflowFile: 'deploy.yml', statusUrl: 'https://app.example.test/.well-known/forgeflow'
};
const store = makeStore({ profile });
const service = new DeploymentService(store, {
listWorkflowRuns: async () => { throw new Error('Actions API disabled'); },
dispatchWorkflow: async () => ({ accepted: true })
}, {
status: async () => ({ head: SHA, clean: true, counts: { changed: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } }),
verifyCommitOnRemoteBranch: async () => ({ valid: true })
}, { info: async () => {}, error: async () => {} });
const operation = await service.deploy({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: 'production', sha: SHA });
assert.equal(operation.status, 'queued');
assert.deepEqual(operation.baselineRunIds, []);
assert.ok(operation.logs.some((line) => line.includes('Could not capture the pre-dispatch run baseline')));
});
test('deployment logs never repeat a line and never carry the Gitea token', () => {
const service = new DeploymentService(makeStore(), {}, {});
const operation = makeOperation({ logs: undefined });
service.appendLog(operation, 'plain line');
service.appendLog(operation, 'plain line');
service.appendLog(operation, 'authorization: token gitea-secret-token');
assert.equal(operation.logs.length, 2, 'a repeated line is not appended twice');
assert.ok(!operation.logs.at(-1).includes('gitea-secret-token'));
for (let index = 0; index < 1200; index += 1) service.appendLog(operation, `line ${index}`);
assert.equal(operation.logs.length, 1000, 'the log is bounded');
assert.equal(operation.logs.at(-1), 'line 1199');
});
test('a repository identity that is not exactly owner/repo is refused', () => {
const service = new DeploymentService(makeStore(), {}, {});
assert.deepEqual(service.splitRepository('jens/app'), { owner: 'jens', repo: 'app' });
for (const value of ['', 'app', 'jens/app/extra', '/app', 'jens/']) {
assert.throws(() => service.splitRepository(value), /Invalid Gitea repository identity/);
}
});
test('deployment is refused without a linked local repository', async () => {
const service = new DeploymentService(makeStore(), {}, {});
await assert.rejects(() => service.deploy({ repository: { fullName: 'jens/app' }, profileId: 'production', sha: SHA }), /linked local repository/);
await assert.rejects(() => service.rollback({ repository: { localPath: '/repo' }, profileId: 'production', targetSha: SHA }), /linked local repository/);
});
test('validation refuses every local state that would deploy something other than the reviewed commit', async () => {
const profile = { id: 'production', environment: 'production', branch: 'main', workflowFile: 'deploy.yml', statusUrl: 'https://app.example.test/status' };
const base = { head: SHA, clean: true, counts: { changed: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } };
const cases = [
[{ ...base, head: 'c'.repeat(40) }, /no longer matches the local repository/],
[{ ...base, branch: { ...base.branch, head: 'feature' } }, /only allows deployments from main/],
[{ ...base, counts: { changed: 2 } }, /Commit local changes/],
[{ ...base, branch: { ...base.branch, ahead: 1 } }, /Push all local commits/],
[{ ...base, branch: { ...base.branch, behind: 1 } }, /Synchronize with Gitea/],
[{ ...base, branch: { ...base.branch, upstream: '' } }, /Publish this branch/]
];
for (const [status, expected] of cases) {
const service = new DeploymentService(makeStore({ profile }), {}, {
status: async () => status,
verifyCommitOnRemoteBranch: async () => ({ valid: true })
});
await assert.rejects(() => service.validateDeploy({ localPath: '/repo' }, profile, SHA), expected);
}
});