Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817
This commit is contained in:
commit
ad8ad252a9
825 files changed
+124216
No files matched your search
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Canonical, machine-readable release identity (docs/adr/ADR-029-v2-r5-final-hardening.md,
|
||||
// brief Phase 9). Computes everything genuinely knowable at *build* time — source
|
||||
// commit, OpenAPI contract hash, frontend build hash, semantic version, build
|
||||
// timestamp — and writes release-manifest.json, which the Dockerfile copies into the
|
||||
// image and the running application reads at startup to enrich GET /api/v1/system/info.
|
||||
//
|
||||
// Deliberately leaves deployment-time-only fields (image tag, image ID, immutable
|
||||
// digest, deployment timestamp, database migration range as actually applied,
|
||||
// inventory snapshot ID, backup ID) as null here — those are not knowable until the
|
||||
// image has actually been built and deployed (the registry/daemon assigns a digest
|
||||
// only after push; a migration range depends on which database the running instance
|
||||
// is actually talking to). Per the brief: "Tag de nieuwe release pas nadat de
|
||||
// daadwerkelijke productie-image ... is gebouwd en succesvol gedeployed" — this script
|
||||
// never invents those values, and a real deployment record (docs/deployments/*.md)
|
||||
// is the source of truth for them, cross-referenced from here only after the fact.
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const outputPath = process.argv[2] ?? path.join(repoRoot, "release-manifest.json");
|
||||
|
||||
function sha256File(relativePath) {
|
||||
const fullPath = path.join(repoRoot, relativePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return null;
|
||||
}
|
||||
const hash = createHash("sha256");
|
||||
hash.update(fs.readFileSync(fullPath));
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
// The built frontend bundle's content-hashed filenames already make every asset's own
|
||||
// hash visible in its filename (Vite's default), but there is no single "the bundle"
|
||||
// file — hash the whole dist/ directory's file list + each file's own bytes instead,
|
||||
// so any output change (including a filename/hash change) changes this value too.
|
||||
function sha256Directory(relativeDir) {
|
||||
const fullDir = path.join(repoRoot, relativeDir);
|
||||
if (!fs.existsSync(fullDir)) {
|
||||
return null;
|
||||
}
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(entryPath);
|
||||
} else {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(fullDir);
|
||||
files.sort();
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const file of files) {
|
||||
hash.update(path.relative(fullDir, file).replace(/\\/g, "/"));
|
||||
hash.update(fs.readFileSync(file));
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function gitCommit() {
|
||||
if (process.env.BUILD_COMMIT && process.env.BUILD_COMMIT !== "development") {
|
||||
return process.env.BUILD_COMMIT;
|
||||
}
|
||||
const result = spawnSync("git", ["rev-parse", "--short=12", "HEAD"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8"
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : "unknown";
|
||||
}
|
||||
|
||||
function readOpenApiVersion() {
|
||||
try {
|
||||
const contents = fs.readFileSync(path.join(repoRoot, "docs/api/openapi.yaml"), "utf8");
|
||||
const match = contents.match(/^\s*version:\s*(.+)$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readSemanticVersion() {
|
||||
const versionFile = path.join(repoRoot, "VERSION");
|
||||
if (fs.existsSync(versionFile)) {
|
||||
return fs.readFileSync(versionFile, "utf8").trim();
|
||||
}
|
||||
// Falls back to the most recent annotated/lightweight tag reachable from HEAD, the
|
||||
// same source `git describe` uses — never invents a version number.
|
||||
const described = spawnSync("git", ["describe", "--tags", "--always"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8"
|
||||
});
|
||||
return described.status === 0 ? described.stdout.trim() : null;
|
||||
}
|
||||
|
||||
// An empty string (as opposed to an unset variable — Docker build args with no
|
||||
// override are still passed as "") means "not actually known" just as much as
|
||||
// unset does; never record it as a real, empty-but-present value.
|
||||
const nonEmpty = (value) => (value && value.trim().length > 0 ? value.trim() : null);
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
semanticVersion: readSemanticVersion(),
|
||||
releaseTag: nonEmpty(process.env.RELEASE_TAG),
|
||||
sourceCommit: gitCommit(),
|
||||
imageRepository: process.env.IMAGE_REPOSITORY ?? "itworx/knowledge-hub",
|
||||
imageTag: nonEmpty(process.env.IMAGE_TAG),
|
||||
imageId: null,
|
||||
imageDigest: null,
|
||||
buildTimestamp: new Date().toISOString(),
|
||||
deploymentTimestamp: null,
|
||||
environment: process.env.BUILD_ENVIRONMENT ?? "production",
|
||||
openApiVersion: readOpenApiVersion(),
|
||||
openApiHash: sha256File("docs/api/openapi.yaml"),
|
||||
frontendBuildHash: sha256Directory("src/frontend/dist"),
|
||||
databaseMigrationRange: null,
|
||||
inventorySnapshotId: null,
|
||||
backupId: null
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||
console.log(`Wrote release manifest to ${outputPath}:`);
|
||||
console.log(JSON.stringify(manifest, null, 2));
|
||||
Reference in new issue
Block a user