- Implemented a migration process to move resources from the legacy owner "default" to the new default owner "local", ensuring no data loss during the transition.
- Updated configuration reference handling to replace legacy `$VAR_`, `$SECRET_`, and `$CONTEXT_` prefixes with mustache-style `{{ vars.name }}`, `{{ secrets.name }}`, and `{{ context.name }}`.
- Enhanced YAML configuration files and scripts to reflect the new mustache syntax, improving consistency across the application.
- Added tests to validate the migration process and ensure proper handling of legacy references.
- Updated documentation to guide users on the new configuration reference format.
269 lines
7.4 KiB
JavaScript
269 lines
7.4 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { DEFAULT_OWNER } from "@jerapah-flow/shared";
|
|
import { db } from "./db.js";
|
|
import { log } from "./logger.js";
|
|
import { EXAMPLE_WORKFLOWS_DIR, WORKFLOWS_DIR } from "./paths.js";
|
|
import { TRASH_WORKFLOWS_DIR } from "./workflow-trash.js";
|
|
import { rewriteLegacyConfigRefsInText } from "./config-ref-rewrite.js";
|
|
|
|
const LEGACY_OWNER = "default";
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @returns {string[]}
|
|
*/
|
|
function listFilesRecursive(dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
/** @type {string[]} */
|
|
const out = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) out.push(...listFilesRecursive(full));
|
|
else out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Move files from legacy owner dir into DEFAULT_OWNER. Skip name collisions.
|
|
* @param {string} rootDir
|
|
* @returns {{ moved: number, skipped: number }}
|
|
*/
|
|
function mergeOwnerDir(rootDir) {
|
|
const fromDir = path.join(rootDir, LEGACY_OWNER);
|
|
const toDir = path.join(rootDir, DEFAULT_OWNER);
|
|
if (!fs.existsSync(fromDir)) return { moved: 0, skipped: 0 };
|
|
|
|
fs.mkdirSync(toDir, { recursive: true });
|
|
let moved = 0;
|
|
let skipped = 0;
|
|
|
|
for (const name of fs.readdirSync(fromDir)) {
|
|
const from = path.join(fromDir, name);
|
|
const to = path.join(toDir, name);
|
|
const st = fs.statSync(from);
|
|
if (!st.isFile()) {
|
|
skipped += 1;
|
|
log.warn({ from }, "owner migrate: skip non-file under legacy owner dir");
|
|
continue;
|
|
}
|
|
if (fs.existsSync(to)) {
|
|
skipped += 1;
|
|
log.warn(
|
|
{ from, to },
|
|
"owner migrate: skip YAML collision (local already has file)",
|
|
);
|
|
continue;
|
|
}
|
|
fs.renameSync(from, to);
|
|
moved += 1;
|
|
}
|
|
|
|
const remaining = fs.existsSync(fromDir) ? fs.readdirSync(fromDir) : [];
|
|
if (remaining.length === 0 && fs.existsSync(fromDir)) {
|
|
fs.rmdirSync(fromDir);
|
|
}
|
|
|
|
return { moved, skipped };
|
|
}
|
|
|
|
/**
|
|
* @param {string} filePath
|
|
* @returns {boolean}
|
|
*/
|
|
function rewriteFileInPlace(filePath) {
|
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
const { text, changed } = rewriteLegacyConfigRefsInText(raw);
|
|
if (!changed) return false;
|
|
fs.writeFileSync(filePath, text, "utf8");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {string} rootDir
|
|
* @returns {number}
|
|
*/
|
|
function rewriteYamlTree(rootDir) {
|
|
let n = 0;
|
|
for (const file of listFilesRecursive(rootDir)) {
|
|
if (!/\.ya?ml$/i.test(file)) continue;
|
|
if (rewriteFileInPlace(file)) n += 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* @param {import("knex").Knex} knex
|
|
* @param {string} table
|
|
* @param {"name" | "file" | null} uniqueCol
|
|
*/
|
|
async function migrateOwnerColumn(knex, table, uniqueCol) {
|
|
const legacyRows = await knex(table).where({ owner: LEGACY_OWNER }).select("*");
|
|
let moved = 0;
|
|
let skipped = 0;
|
|
for (const row of legacyRows) {
|
|
if (uniqueCol != null) {
|
|
const conflict = await knex(table)
|
|
.where({ owner: DEFAULT_OWNER, [uniqueCol]: row[uniqueCol] })
|
|
.first();
|
|
if (conflict) {
|
|
skipped += 1;
|
|
log.warn(
|
|
{ table, id: row.id, [uniqueCol]: row[uniqueCol] },
|
|
"owner migrate: skip row collision",
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
await knex(table).where({ id: row.id }).update({ owner: DEFAULT_OWNER });
|
|
moved += 1;
|
|
}
|
|
return { moved, skipped };
|
|
}
|
|
|
|
/**
|
|
* @param {import("knex").Knex} knex
|
|
*/
|
|
async function migrateWorkflowRuns(knex) {
|
|
const n = await knex("workflow_runs")
|
|
.where({ owner: LEGACY_OWNER })
|
|
.update({ owner: DEFAULT_OWNER });
|
|
return { moved: Number(n) || 0, skipped: 0 };
|
|
}
|
|
|
|
/**
|
|
* @param {import("knex").Knex} knex
|
|
*/
|
|
async function migrateWorkflowRevisionsOwner(knex) {
|
|
const n = await knex("workflow_revisions")
|
|
.where({ owner: LEGACY_OWNER })
|
|
.update({ owner: DEFAULT_OWNER });
|
|
return { moved: Number(n) || 0, skipped: 0 };
|
|
}
|
|
|
|
/**
|
|
* @param {import("knex").Knex} knex
|
|
*/
|
|
async function migrateScriptStateNamespaces(knex) {
|
|
const rows = await knex("script_state")
|
|
.where("namespace", "like", `${LEGACY_OWNER}/%`)
|
|
.select("namespace", "key");
|
|
let moved = 0;
|
|
let skipped = 0;
|
|
for (const row of rows) {
|
|
const nextNs = `${DEFAULT_OWNER}${row.namespace.slice(LEGACY_OWNER.length)}`;
|
|
const conflict = await knex("script_state")
|
|
.where({ namespace: nextNs, key: row.key })
|
|
.first();
|
|
if (conflict) {
|
|
skipped += 1;
|
|
log.warn(
|
|
{ namespace: row.namespace, key: row.key, nextNs },
|
|
"owner migrate: skip script_state collision",
|
|
);
|
|
continue;
|
|
}
|
|
await knex("script_state")
|
|
.where({ namespace: row.namespace, key: row.key })
|
|
.update({ namespace: nextNs });
|
|
moved += 1;
|
|
}
|
|
return { moved, skipped };
|
|
}
|
|
|
|
/**
|
|
* @param {import("knex").Knex} knex
|
|
*/
|
|
async function rewriteDbConfigStrings(knex) {
|
|
let profiles = 0;
|
|
let revisions = 0;
|
|
|
|
const profileRows = await knex("profiles").select("id", "config");
|
|
for (const row of profileRows) {
|
|
const { text, changed } = rewriteLegacyConfigRefsInText(String(row.config ?? ""));
|
|
if (!changed) continue;
|
|
await knex("profiles").where({ id: row.id }).update({ config: text });
|
|
profiles += 1;
|
|
}
|
|
|
|
const revisionRows = await knex("workflow_revisions").select("id", "content");
|
|
for (const row of revisionRows) {
|
|
const { text, changed } = rewriteLegacyConfigRefsInText(String(row.content ?? ""));
|
|
if (!changed) continue;
|
|
await knex("workflow_revisions").where({ id: row.id }).update({ content: text });
|
|
revisions += 1;
|
|
}
|
|
|
|
return { profiles, revisions };
|
|
}
|
|
|
|
/**
|
|
* One-shot: move owner `default` → `local`, rewrite prefix refs to mustache.
|
|
* Idempotent when there is no remaining `default` data / prefix refs.
|
|
*/
|
|
export async function migrateDefaultOwnerIfNeeded() {
|
|
const knex = db;
|
|
|
|
const yamlLive = mergeOwnerDir(WORKFLOWS_DIR);
|
|
const yamlTrash = mergeOwnerDir(TRASH_WORKFLOWS_DIR);
|
|
|
|
const variables = await migrateOwnerColumn(knex, "variables", "name");
|
|
const secrets = await migrateOwnerColumn(knex, "secrets", "name");
|
|
const profiles = await migrateOwnerColumn(knex, "profiles", "name");
|
|
const trash = await migrateOwnerColumn(knex, "workflow_trash", "file");
|
|
const runs = await migrateWorkflowRuns(knex);
|
|
const revisionsOwner = await migrateWorkflowRevisionsOwner(knex);
|
|
const scriptState = await migrateScriptStateNamespaces(knex);
|
|
|
|
const yamlRewritten =
|
|
rewriteYamlTree(path.join(WORKFLOWS_DIR, DEFAULT_OWNER)) +
|
|
rewriteYamlTree(path.join(TRASH_WORKFLOWS_DIR, DEFAULT_OWNER)) +
|
|
rewriteYamlTree(path.join(WORKFLOWS_DIR, LEGACY_OWNER)) +
|
|
rewriteYamlTree(path.join(TRASH_WORKFLOWS_DIR, LEGACY_OWNER));
|
|
|
|
let examplesRewritten = 0;
|
|
if (fs.existsSync(EXAMPLE_WORKFLOWS_DIR)) {
|
|
examplesRewritten = rewriteYamlTree(EXAMPLE_WORKFLOWS_DIR);
|
|
}
|
|
|
|
const dbStrings = await rewriteDbConfigStrings(knex);
|
|
|
|
const summary = {
|
|
yamlLive,
|
|
yamlTrash,
|
|
variables,
|
|
secrets,
|
|
profiles,
|
|
trash,
|
|
runs,
|
|
revisionsOwner,
|
|
scriptState,
|
|
yamlRewritten,
|
|
examplesRewritten,
|
|
dbStrings,
|
|
};
|
|
|
|
const touched =
|
|
yamlLive.moved +
|
|
yamlTrash.moved +
|
|
variables.moved +
|
|
secrets.moved +
|
|
profiles.moved +
|
|
trash.moved +
|
|
runs.moved +
|
|
revisionsOwner.moved +
|
|
scriptState.moved +
|
|
yamlRewritten +
|
|
examplesRewritten +
|
|
dbStrings.profiles +
|
|
dbStrings.revisions >
|
|
0;
|
|
|
|
if (touched) {
|
|
log.info(summary, "migrated owner default → local and rewrote config refs");
|
|
}
|
|
|
|
return summary;
|
|
}
|