Files
dougal-software/lib/www/server/lib/version.js
D. Berge a58cce8565 Add /version/history endpoint to API.
Retrieves Git tag annotations.
2025-07-26 10:58:42 +02:00

174 lines
4.1 KiB
JavaScript

const semver = require("semver");
const { exec } = require("child_process");
const { readFileSync } = require('fs');
const { pool } = require('./db/connection');
const { info } = require('./db');
const pkg = require("../package.json");
const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename);
function compatible () {
return Promise.all([
compatible_schema()
]);
}
/** Report whether the database schema version is
* compatible with the version required by this server.
*
* The current schema version is retrieved from the
* public.info table.
*
* The wanted version is retrieved from package.json
* (config.db_schema).
*
* @returns true if the versions are compatible,
* false otherwise.
*/
function compatible_schema () {
return new Promise ( async (resolve, reject) => {
const current = await schema_version();
const wanted = pkg.config.wanted.db_schema;
const component = "schema";
if (semver.satisfies(current, wanted)) {
resolve({current, wanted, component});
} else {
reject({current, wanted, component});
}
});
}
async function db_version () {
const client = await pool.connect();
let res;
try {
res = (await client.query("SELECT version(), PostGIS_Version();"))?.rows[0];
} catch (err) {
ERROR(err);
} finally {
client.release();
}
return res;
}
async function os_version () {
try {
return Object.fromEntries(
readFileSync("/etc/os-release")
.toString("utf-8")
.split("\n")
.map(i =>
i.split("=", 2)
.map(i => i.replace(/^"|"$/g, ""))
)
.filter(i =>
["ID", "NAME", "PRETTY_NAME", "VERSION", "VERSION_ID"].includes(i[0])
)
);
} catch (err) {
ERROR(err);
}
}
async function schema_version () {
return await info.get(null, "version/db_schema");
}
/** Return software name.
*
*/
function app_name () {
return pkg.name ?? pkg.description ?? "Unknown";
}
/** Return software version, from Git if possible.
*
*/
async function describe () {
return new Promise( (resolve, reject) => {
const cmd = `git describe || echo git+$(git describe --always);`;
exec(cmd, {cwd: __dirname}, (error, stdout, stderr) => {
if (error) {
reject(error);
}
if (stdout) {
resolve(stdout.trim());
} else {
// Most likely not installed from Git, use the
// version number in package.json.
resolve(pkg.version ?? "Unknown")
}
})
});
}
/** Return version history, from git tags
*
*/
async function history (count=5, lines=15) {
return new Promise( (resolve, reject) => {
const separator2 = String(Math.random()).substring(2)+String(Math.random()).substring(2);
const separator1 = separator2.repeat(2);
const cmd = `for tag in $(git tag --sort=-version:refname |head -n ${count}); do printf "\n${separator1}\n"; echo "$tag"; printf "\n${separator2}\n"; git --no-pager tag -n${lines} "$tag"; done`;
exec(cmd, {cwd: __dirname}, (error, stdout, stderr) => {
if (error) {
reject(error);
}
if (stdout) {
const result = stdout
.split(separator1) // Separate each tag
.map(i => i.trim()) // Trim spaces
.filter(i => i) // Filter empty lines
.map(i => i // Now for each tag…
.split(separator2) // …separate the tag from the description
.map(i => i.trim()) // trim spaces in both
.filter( i => i) // and remove empty lines
)
resolve(Object.fromEntries(result));
// resolve(result.map( ([k, v]) => ({tag: k, notes: v}) ));
} else {
// Most likely not installed from Git, use the
// version number in package.json.
resolve()
}
})
});
}
function version_old () {
return pkg.version;
}
async function version () {
const name = app_name();
const server = pkg.version;
const tag = await describe();
const schema = await schema_version();
const db = await db_version();
const os = await os_version();
const compatibility = [
await compatible_schema()
]
return {
name,
server,
tag,
...pkg?.config?.versions,
schema,
db,
os,
compatibility
}
}
version.compatible = compatible;
version.name = app_name;
version.describe = describe;
version.history = history;
module.exports = version;