Migrated to using Bun instead of Node and PNPM. This also brings in a Devcontainer which enables quick and easy development of the project. Additionally this adds connectivity to S3 (with a default Minio server pre-created) this enables Files to be uploaded against Mantises. There's also a new Internal Notes feature to store arbitrary text notes against a Mantis.
This commit is contained in:
parent
80ca48be70
commit
3b846b8c8e
23 changed files with 3210 additions and 6490 deletions
236
project-setup.js
Normal file
236
project-setup.js
Normal file
|
@ -0,0 +1,236 @@
|
|||
import { randomBytes } from 'crypto';
|
||||
|
||||
const c = (colour) =>
|
||||
{
|
||||
if(colour === 'reset')
|
||||
{
|
||||
return '\x1b[0m';
|
||||
}
|
||||
return Bun.color(colour, 'ansi');
|
||||
};
|
||||
|
||||
const log = (message) => console.log(message);
|
||||
const logInfo = (message) => log(c('blue') + '[INFO] ' + c('reset') + message);
|
||||
const logSuccess = (message) => log(c('green') + '[SUCCESS] ' + c('reset') + message);
|
||||
const logWarn = (message) => log(c('yellow') + '[WARN] ' + c('reset') + message);
|
||||
const logError = (message) => log(c('red') + '[ERROR] ' + c('reset') + message);
|
||||
|
||||
const ENV_FILE = '.env';
|
||||
const ENV_EXAMPLE_FILE = '.env.example';
|
||||
const DOCKER_COMPOSE_FILE = 'docker-compose.yml';
|
||||
const DOCKER_COMPOSE_EXAMPLE_FILE = 'docker-compose-example.yml';
|
||||
|
||||
async function generateSecureRandomString(bytes = 32)
|
||||
{
|
||||
return randomBytes(bytes).toString('hex');
|
||||
}
|
||||
|
||||
async function ensureFileFromExample(targetPath, examplePath)
|
||||
{
|
||||
logInfo(`Checking for ${targetPath}...`);
|
||||
const targetExists = await Bun.file(targetPath).exists();
|
||||
|
||||
if (!targetExists)
|
||||
{
|
||||
logWarn(`${targetPath} not found. Copying from ${examplePath}...`);
|
||||
const exampleExists = await Bun.file(examplePath).exists();
|
||||
if (!exampleExists)
|
||||
{
|
||||
logError(`${examplePath} not found. Cannot create ${targetPath}. Please ensure ${examplePath} exists.`);
|
||||
// Don't exit immediately, maybe other checks can proceed.
|
||||
// Consider if this should be a fatal error depending on project needs.
|
||||
return { copied: false, error: true };
|
||||
}
|
||||
try
|
||||
{
|
||||
const exampleContent = await Bun.file(examplePath).text();
|
||||
await Bun.write(targetPath, exampleContent);
|
||||
logSuccess(`Copied ${examplePath} to ${targetPath}.`);
|
||||
return { copied: true, error: false }; // Indicates file was copied
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logError(`Failed to copy ${examplePath} to ${targetPath}: ${error.message}`);
|
||||
return { copied: false, error: true }; // Indicate error
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logInfo(`${targetPath} already exists.`);
|
||||
return { copied: false, error: false }; // Indicates file already existed
|
||||
}
|
||||
}
|
||||
|
||||
const secrets = new Map();
|
||||
async function replacePlaceholders(filePath)
|
||||
{
|
||||
logInfo(`Checking for placeholders in ${filePath}...`);
|
||||
try
|
||||
{
|
||||
let content = await Bun.file(filePath).text();
|
||||
const placeholderRegex = /{{\s*([A-Z0-9_]+)\s*}}/g;
|
||||
const placeholders = [...content.matchAll(placeholderRegex)];
|
||||
const uniquePlaceholderNames = [...new Set(placeholders.map(match => match[1]))];
|
||||
|
||||
if (uniquePlaceholderNames.length === 0)
|
||||
{
|
||||
logInfo(`No placeholders found in ${filePath}.`);
|
||||
return true; // Indicate success (no action needed)
|
||||
}
|
||||
|
||||
logInfo(`Found placeholders: ${uniquePlaceholderNames.join(', ')}. Generating secrets...`);
|
||||
for (const name of uniquePlaceholderNames)
|
||||
{
|
||||
// Reuse existing secret if already generated for another file in this run
|
||||
if (!secrets.has(name))
|
||||
{
|
||||
secrets.set(name, await generateSecureRandomString());
|
||||
}
|
||||
}
|
||||
|
||||
let replacementsMade = false;
|
||||
content = content.replace(placeholderRegex, (match, name) =>
|
||||
{
|
||||
const secret = secrets.get(name);
|
||||
if (secret)
|
||||
{
|
||||
replacementsMade = true;
|
||||
return secret;
|
||||
}
|
||||
return match; // Return original match if name not found (shouldn't happen with current logic)
|
||||
});
|
||||
|
||||
if (replacementsMade)
|
||||
{
|
||||
await Bun.write(filePath, content);
|
||||
logSuccess(`Replaced placeholders in ${filePath} with generated secrets.`);
|
||||
}
|
||||
else
|
||||
{
|
||||
logInfo(`No placeholder values needed replacement in ${filePath}.`);
|
||||
}
|
||||
return true; // Indicate success
|
||||
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logError(`Failed to process placeholders in ${filePath}: ${error.message}`);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDockerComposeRunning()
|
||||
{
|
||||
logInfo('Ensuring Docker Compose services are running...');
|
||||
// Check if docker-compose.yml exists first
|
||||
const composeFileExists = await Bun.file(DOCKER_COMPOSE_FILE).exists();
|
||||
if (!composeFileExists)
|
||||
{
|
||||
logWarn(`Skipping Docker Compose setup because ${DOCKER_COMPOSE_FILE} does not exist.`);
|
||||
return true; // Not an error, just skipping
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
logInfo(`Running docker compose -f ${DOCKER_COMPOSE_FILE} up -d...`);
|
||||
const proc = Bun.spawn({
|
||||
cmd: ['docker', 'compose', '-f', DOCKER_COMPOSE_FILE, 'up', '-d'],
|
||||
stdout: 'inherit', // Pipe output to the setup script's stdout
|
||||
stderr: 'inherit', // Pipe errors to the setup script's stderr
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
if (exitCode === 0)
|
||||
{
|
||||
logSuccess('Docker Compose services started successfully (or were already running).');
|
||||
return true; // Indicate success
|
||||
}
|
||||
else
|
||||
{
|
||||
logError(`Docker Compose command failed with exit code ${exitCode}.`);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logError(`Failed to run Docker Compose: ${error.message}`);
|
||||
logError('Please ensure Docker is installed, running, and you have permissions to use it.');
|
||||
return false; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
async function main()
|
||||
{
|
||||
let overallSuccess = true;
|
||||
console.log(c('aqua') +` _____ _ _ _____ _ _
|
||||
/ ____| | | | | __ \\ (_) | |
|
||||
| (___ | |_ _ _| | ___| |__) |__ _ _ __ | |_
|
||||
\\___ \\| __| | | | |/ _ \\ ___/ _ \\| | '_ \\| __|
|
||||
____) | |_| |_| | | __/ | | (_) | | | | | |_
|
||||
|_____/ \\__|\\__, |_|\\___|_| \\___/|_|_| |_|\\__|
|
||||
__/ |
|
||||
|___/ ` + c('reset'));
|
||||
|
||||
logInfo('Starting project setup validation...');
|
||||
|
||||
// Ensure .env file exists and replace placeholders if copied
|
||||
const envResult = await ensureFileFromExample(ENV_FILE, ENV_EXAMPLE_FILE);
|
||||
if (envResult.error)
|
||||
{
|
||||
overallSuccess = false;
|
||||
}
|
||||
else if (envResult.copied)
|
||||
{
|
||||
if (!await replacePlaceholders(ENV_FILE))
|
||||
{
|
||||
overallSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure docker-compose.yml exists and replace placeholders if copied
|
||||
const composeResult = await ensureFileFromExample(DOCKER_COMPOSE_FILE, DOCKER_COMPOSE_EXAMPLE_FILE);
|
||||
if (composeResult.error)
|
||||
{
|
||||
overallSuccess = false;
|
||||
}
|
||||
else if (composeResult.copied)
|
||||
{
|
||||
if (!await replacePlaceholders(DOCKER_COMPOSE_FILE))
|
||||
{
|
||||
overallSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only attempt to run docker compose if the previous steps were generally successful
|
||||
// and the compose file actually exists now.
|
||||
if (overallSuccess && await Bun.file(DOCKER_COMPOSE_FILE).exists())
|
||||
{
|
||||
if (!await ensureDockerComposeRunning())
|
||||
{
|
||||
overallSuccess = false;
|
||||
}
|
||||
}
|
||||
else if (!await Bun.file(DOCKER_COMPOSE_FILE).exists())
|
||||
{
|
||||
logWarn(`Skipping Docker Compose execution as ${DOCKER_COMPOSE_FILE} is missing.`);
|
||||
// Not necessarily a failure if the example file was also missing.
|
||||
}
|
||||
|
||||
if (overallSuccess)
|
||||
{
|
||||
logSuccess('Project setup validation completed successfully.');
|
||||
}
|
||||
else
|
||||
{
|
||||
logError('Project setup validation failed. Please check the logs above.');
|
||||
process.exit(1); // Exit with error code if any step failed
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err =>
|
||||
{
|
||||
logError(`Unhandled error during setup: ${err.message}\n${err.stack}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue