DX-13: both shell scripts ran bare 'docker compose', which has no config
file to find in the layout docs/self-hosting.md tells people to use (only
docker-compose.selfhost.yml is downloaded) — they now fall back to it, while
a source checkout keeps resolving docker-compose.yml as before. The two
PowerShell scripts moved the dump through the PS pipeline, which corrupts
binary on 5.1 and cannot express -Encoding Byte at all on 7 ('Byte' is not a
supported encoding name), so they simply failed there; they now write inside
the container and 'docker compose cp' it across. backup.sh also wrote its
output straight into backups/, leaving a truncated file that looks like a
real backup when pg_dump fails — it writes a .part and promotes on success.
Both restores report a non-zero pg_restore rather than printing 'Restored'
over it. Docs now state that a backup is the dump PLUS .env: without
TOKEN_ENCRYPTION_KEY every encrypted secret in the restored database is
unreadable, which the old one-line 'back up the volume' never said.
DX-14: 'latest' moves with every release and migrations run on every boot,
so self-hosters had no way to choose when they move and no way back. Docs
gain IMAGE_TAG pinning, a rollback recipe (image-only vs schema-bearing) and
the migration-failed-at-boot case; CHANGELOG.md is generated from
releaseNotes.ts so the file cannot drift from what the app shows, and
'siftlode release' refuses on a stale one rather than regenerating it into
the clean tree it just demanded.
Verified: dump + restore round-trip on both platforms against localdev into
a scratch database (8 users, 262822 videos, schema 0082_invite_lang), and
IMAGE_TAG=0.80.0 resolves on both services in 'compose config'.
87 lines
3.5 KiB
JavaScript
87 lines
3.5 KiB
JavaScript
// Generates CHANGELOG.md from frontend/src/lib/releaseNotes.ts — the same entries the app shows
|
|
// under Release Notes, so a self-hoster deciding whether to upgrade reads the same text an
|
|
// in-app user does, without having to install the app first (DX-14).
|
|
//
|
|
// Why derive rather than hand-write: two lists of "what changed in 0.80.0" WILL drift, and the
|
|
// in-app one is the one that gets written during a release. This makes the file a build product of
|
|
// it. RELEASE_NOTES is a plain data literal by design, so it is imported by stripping the single TS
|
|
// type annotation off the declaration — no transpiler, no dependency.
|
|
//
|
|
// node scripts/gen-changelog.mjs # write CHANGELOG.md
|
|
// node scripts/gen-changelog.mjs --check # exit 1 if CHANGELOG.md is stale (used by `siftlode release`)
|
|
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const SOURCE = path.join(root, "frontend", "src", "lib", "releaseNotes.ts");
|
|
const TARGET = path.join(root, "CHANGELOG.md");
|
|
|
|
async function loadEntries() {
|
|
const src = await readFile(SOURCE, "utf8");
|
|
const decl = "export const RELEASE_NOTES";
|
|
const at = src.indexOf(decl);
|
|
if (at === -1) {
|
|
throw new Error(`${SOURCE} no longer declares ${decl} — the generator's assumption broke, fix it rather than hand-writing the changelog.`);
|
|
}
|
|
const body = src.slice(at).replace(/^export const RELEASE_NOTES\s*:[^=]*=/, "export const RELEASE_NOTES =");
|
|
const mod = await import(`data:text/javascript,${encodeURIComponent(body)}`);
|
|
if (!Array.isArray(mod.RELEASE_NOTES) || mod.RELEASE_NOTES.length === 0) {
|
|
throw new Error("RELEASE_NOTES imported as something other than a non-empty array.");
|
|
}
|
|
return mod.RELEASE_NOTES;
|
|
}
|
|
|
|
function section(title, items) {
|
|
if (!items || items.length === 0) return "";
|
|
return `### ${title}\n\n${items.map((line) => `- ${line}`).join("\n")}\n\n`;
|
|
}
|
|
|
|
function render(entries) {
|
|
const head = [
|
|
"# Changelog",
|
|
"",
|
|
"Every released version of Siftlode, newest first.",
|
|
"",
|
|
"**Generated from `frontend/src/lib/releaseNotes.ts` by `scripts/gen-changelog.mjs` — do not edit by hand;**",
|
|
"edit the release notes and re-run the generator.",
|
|
"",
|
|
"Upgrading, pinning a version and rolling back: [docs/self-hosting.md](docs/self-hosting.md#upgrading).",
|
|
"",
|
|
"",
|
|
].join("\n");
|
|
|
|
const body = entries
|
|
.map((e) => {
|
|
const sha = e.sha ? ` (\`${e.sha}\`)` : "";
|
|
let out = `## ${e.version} — ${e.date}${sha}\n\n`;
|
|
if (e.summary) out += `${e.summary}\n\n`;
|
|
out += section("Features", e.features);
|
|
out += section("Fixes", e.fixes);
|
|
out += section("Internal", e.chores);
|
|
return out;
|
|
})
|
|
.join("");
|
|
|
|
return `${head}${body}`.replace(/\n{3,}$/, "\n");
|
|
}
|
|
|
|
const entries = await loadEntries();
|
|
const rendered = render(entries);
|
|
|
|
if (process.argv.includes("--check")) {
|
|
const current = await readFile(TARGET, "utf8").catch(() => null);
|
|
if (current !== rendered) {
|
|
console.error(
|
|
`CHANGELOG.md is out of date with releaseNotes.ts (newest entry: ${entries[0].version}).\n` +
|
|
"Run: node scripts/gen-changelog.mjs — then commit the result.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log(`CHANGELOG.md is current (${entries.length} releases, newest ${entries[0].version}).`);
|
|
} else {
|
|
await writeFile(TARGET, rendered, "utf8");
|
|
console.log(`Wrote CHANGELOG.md — ${entries.length} releases, newest ${entries[0].version}.`);
|
|
}
|