Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 170 additions & 71 deletions internals/ci/src/cmds/version/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { doBuildPkgs } from "@oko-wallet-ci/cmds/build_pkgs";
import { expectSuccess } from "@oko-wallet-ci/expect";
import { doBuildSDK } from "@oko-wallet-ci/cmds/build_sdk";

const WILD_CHARACTER_VERSION = "workspace:*";

function getPackageJsonPaths(): string[] {
const lernaJsonPath = path.join(paths.root, "lerna.json");
const lernaJson = JSON.parse(fs.readFileSync(lernaJsonPath, "utf-8"));
Expand All @@ -18,107 +16,181 @@ function getPackageJsonPaths(): string[] {
return packages.map((pkg) => path.join(paths.root, pkg, "package.json"));
}

interface WorkspaceDepInfo {
filePath: string;
depName: string;
depVersion: string;
}
function buildWorkspaceVersionMap(): Map<string, string> {
const versionMap = new Map<string, string>();
const packageJsonPaths = getPackageJsonPaths();

interface WorkspaceDep {
name: string;
version: string;
for (const pkgPath of packageJsonPaths) {
try {
const content = fs.readFileSync(pkgPath, "utf-8");
const pkg = JSON.parse(content);
if (pkg.name && pkg.version) {
versionMap.set(pkg.name, pkg.version);
}
} catch (err) {
console.warn(" Failed to read %s: %s", pkgPath, err);
}
}

return versionMap;
}

function findWorkspaceDeps(pkg: Record<string, unknown>): WorkspaceDep[] {
function replaceWorkspaceVersions() {
console.log("Replacing workspace:* with actual versions...");

const versionMap = buildWorkspaceVersionMap();
const packageJsonPaths = getPackageJsonPaths();
const depFields = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
const found: { name: string; version: string }[] = [];

for (const field of depFields) {
const deps = pkg[field] as Record<string, string>;

if (deps && typeof deps === "object") {
for (const [name, version] of Object.entries(deps)) {
if (
typeof version === "string" &&
version.startsWith(WILD_CHARACTER_VERSION)
) {
found.push({ name, version });
}
}
}
}

return found;
}

function checkWorkspaceVersions() {
console.log('Checking for "workspace:" versions in publishable pkgs');

const packageJsonFiles = getPackageJsonPaths();
const issues: WorkspaceDepInfo[] = [];
let replacedCount = 0;

for (const filePath of packageJsonFiles) {
for (const pkgPath of packageJsonPaths) {
try {
const content = fs.readFileSync(filePath, "utf-8");
const content = fs.readFileSync(pkgPath, "utf-8");
const pkg = JSON.parse(content);

// Skip private packages
if (pkg.private === true) {
continue;
}

const workspaceDeps = findWorkspaceDeps(pkg);
for (const dep of workspaceDeps) {
issues.push({ filePath, depName: dep.name, depVersion: dep.version });
let modified = false;

for (const field of depFields) {
const deps = pkg[field];
if (!deps || typeof deps !== "object") {
continue;
}

for (const [depName, depVersion] of Object.entries(deps)) {
if (
typeof depVersion === "string" &&
depVersion.startsWith("workspace:")
) {
const actualVersion = versionMap.get(depName);
if (actualVersion) {
// workspace:* → ^X.Y.Z, workspace:^ → ^X.Y.Z, workspace:~ → ~X.Y.Z
let prefix = "^";
if (depVersion === "workspace:~") {
prefix = "~";
} else if (depVersion === "workspace:*") {
prefix = "^";
} else if (depVersion.startsWith("workspace:^")) {
prefix = "^";
}
deps[depName] = `${prefix}${actualVersion}`;
modified = true;
replacedCount += 1;
}
}
}
}

if (modified) {
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
console.log(" Updated: %s", pkgPath);
}
} catch (err) {
console.warn(
"%s failed to read %s, err: %s",
chalk.bold.red("error"),
filePath,
err instanceof Error ? err.message : err,
);

process.exit(1);
console.warn(" Failed to process %s: %s", pkgPath, err);
}
}

if (issues.length > 0) {
console.error(
`%s Found "workspace:" versions in publishable packages`,
chalk.bold.red("error"),
);
console.log(
"%s Replaced %d workspace:* references",
chalk.green.bold("Done"),
replacedCount,
);
}

function getChangedPackages(
beforeMap: Map<string, string>,
afterMap: Map<string, string>,
): Array<{ name: string; version: string }> {
const changed: Array<{ name: string; version: string }> = [];

for (const issue of issues) {
console.error(
` ${issue.filePath}: ${issue.depName} -> ${issue.depVersion}`,
);
for (const [name, version] of afterMap) {
const beforeVersion = beforeMap.get(name);
if (beforeVersion !== version) {
changed.push({ name, version });
}
}

console.error(
`Please replace wildcard versions ("%s") with actual version numbers
beforeversioning.`,
WILD_CHARACTER_VERSION,
);
return changed;
}

function createGitCommitAndTags(
changedPackages: Array<{ name: string; version: string }>,
) {
console.log("Creating git commit and tags...");

process.exit(1);
if (changedPackages.length === 0) {
console.log("No packages were changed, skipping commit and tags");
return;
}

// Stage all changes
const addRet = spawnSync("git", ["add", "."], {
cwd: paths.root,
stdio: "inherit",
});
expectSuccess(addRet, "git add failed");

// Build commit message with version info
const versionList = changedPackages
.map((pkg) => `- ${pkg.name}@${pkg.version}`)
.join("\n");
const commitMessage = `chore: publish

${versionList}`;

// Create commit
const commitRet = spawnSync("git", ["commit", "-m", commitMessage], {
cwd: paths.root,
stdio: "inherit",
});
expectSuccess(commitRet, "git commit failed");

// Create tags only for changed packages
for (const pkg of changedPackages) {
const tag = `${pkg.name}@${pkg.version}`;
console.log(" Creating tag: %s", tag);
const tagRet = spawnSync("git", ["tag", "-a", tag, "-m", tag], {
cwd: paths.root,
stdio: "inherit",
});
if (tagRet.status !== 0) {
console.warn(" Tag %s already exists, skipping", tag);
}
}

console.log(
`%s No "workspace:" versions found in publishable packages`,
chalk.bold.green("Done"),
"%s Created commit and %d tags",
chalk.green.bold("Done"),
changedPackages.length,
);

// Push commit and tags to remote
console.log("Pushing commit and tags to origin...");
const pushRet = spawnSync(
"git",
["push", "-u", "origin", "HEAD", "--follow-tags"],
{
cwd: paths.root,
stdio: "inherit",
},
);
expectSuccess(pushRet, "git push failed");
console.log("%s Pushed to origin", chalk.green.bold("Done"));
}

export async function version(..._args: any[]) {
console.log("Start versioning packages");

checkWorkspaceVersions();

console.log("We will re-build the packages now just to make sure\n");

await doBuildPkgs();
Expand All @@ -139,8 +211,35 @@ export async function version(..._args: any[]) {
});
expectSuccess(fetchRet, "publish failed");

spawnSync("yarn", ["lerna", "version", "--no-private"], {
cwd: paths.root,
stdio: "inherit",
});
// Save version map before lerna version
const beforeVersionMap = buildWorkspaceVersionMap();

spawnSync(
"yarn",
["lerna", "version", "--no-private", "--no-git-tag-version"],
{
cwd: paths.root,
stdio: "inherit",
},
);

// Get version map after lerna version and find changed packages
const afterVersionMap = buildWorkspaceVersionMap();
const changedPackages = getChangedPackages(beforeVersionMap, afterVersionMap);

try {
replaceWorkspaceVersions();
} catch (err) {
console.error(
"%s replaceWorkspaceVersions failed, rolling back...",
chalk.bold.red("Error"),
);
spawnSync("git", ["checkout", "--", "."], {
cwd: paths.root,
stdio: "inherit",
});
throw err;
}

createGitCommitAndTags(changedPackages);
}