@ucdjs/release-scripts 0.1.0-beta.36 → 0.1.0-beta.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -29,6 +29,8 @@ interface ReleaseScriptsOptionsInput {
29
29
  npm?: {
30
30
  otp?: string;
31
31
  provenance?: boolean;
32
+ access?: "public" | "restricted";
33
+ runBuild?: boolean;
32
34
  };
33
35
  prompts?: {
34
36
  versions?: boolean;
package/dist/index.mjs CHANGED
@@ -362,7 +362,9 @@ function normalizeReleaseScriptsOptions(options) {
362
362
  } : DEFAULT_TYPES,
363
363
  npm: {
364
364
  otp: npm.otp,
365
- provenance: npm.provenance ?? true
365
+ provenance: npm.provenance ?? true,
366
+ access: npm.access ?? "public",
367
+ runBuild: npm.runBuild ?? true
366
368
  },
367
369
  prompts: {
368
370
  versions: prompts.versions ?? !isCI,
@@ -482,7 +484,19 @@ async function checkoutBranch(branch, workspaceRoot) {
482
484
  }
483
485
  return ok(false);
484
486
  } catch (error) {
485
- return err(toGitError("checkoutBranch", error));
487
+ const gitError = toGitError("checkoutBranch", error);
488
+ logger.error(`Git checkout failed: ${gitError.message}`);
489
+ if (gitError.stderr) logger.error(`Git stderr: ${gitError.stderr}`);
490
+ try {
491
+ const branchResult = await run("git", ["branch", "-a"], { nodeOptions: {
492
+ cwd: workspaceRoot,
493
+ stdio: "pipe"
494
+ } });
495
+ logger.verbose(`Available branches:\n${branchResult.stdout}`);
496
+ } catch {
497
+ logger.verbose("Could not list available branches");
498
+ }
499
+ return err(gitError);
486
500
  }
487
501
  }
488
502
  async function pullLatestChanges(branch, workspaceRoot) {
@@ -656,6 +670,60 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
656
670
  return err(toGitError("getGroupedFilesByCommitSha", error));
657
671
  }
658
672
  }
673
+ /**
674
+ * Create a git tag for a package release
675
+ * @param packageName - The package name (e.g., "@scope/name")
676
+ * @param version - The version to tag (e.g., "1.2.3")
677
+ * @param workspaceRoot - The root directory of the workspace
678
+ * @returns Result indicating success or failure
679
+ */
680
+ async function createPackageTag(packageName, version, workspaceRoot) {
681
+ const tagName = `${packageName}@${version}`;
682
+ try {
683
+ logger.info(`Creating tag: ${farver.green(tagName)}`);
684
+ await runIfNotDry("git", ["tag", tagName], { nodeOptions: {
685
+ cwd: workspaceRoot,
686
+ stdio: "pipe"
687
+ } });
688
+ return ok(void 0);
689
+ } catch (error) {
690
+ return err(toGitError("createPackageTag", error));
691
+ }
692
+ }
693
+ /**
694
+ * Push a specific tag to the remote repository
695
+ * @param tagName - The tag name to push
696
+ * @param workspaceRoot - The root directory of the workspace
697
+ * @returns Result indicating success or failure
698
+ */
699
+ async function pushTag(tagName, workspaceRoot) {
700
+ try {
701
+ logger.info(`Pushing tag: ${farver.green(tagName)}`);
702
+ await runIfNotDry("git", [
703
+ "push",
704
+ "origin",
705
+ tagName
706
+ ], { nodeOptions: {
707
+ cwd: workspaceRoot,
708
+ stdio: "pipe"
709
+ } });
710
+ return ok(void 0);
711
+ } catch (error) {
712
+ return err(toGitError("pushTag", error));
713
+ }
714
+ }
715
+ /**
716
+ * Create and push a package tag in one operation
717
+ * @param packageName - The package name
718
+ * @param version - The version to tag
719
+ * @param workspaceRoot - The root directory of the workspace
720
+ * @returns Result indicating success or failure
721
+ */
722
+ async function createAndPushPackageTag(packageName, version, workspaceRoot) {
723
+ const createResult = await createPackageTag(packageName, version, workspaceRoot);
724
+ if (!createResult.ok) return createResult;
725
+ return pushTag(`${packageName}@${version}`, workspaceRoot);
726
+ }
659
727
 
660
728
  //#endregion
661
729
  //#region src/core/changelog.ts
@@ -1281,6 +1349,51 @@ function getAllAffectedPackages(graph, changedPackages) {
1281
1349
  return affected;
1282
1350
  }
1283
1351
  /**
1352
+ * Calculate the order in which packages should be published
1353
+ *
1354
+ * Performs topological sorting to ensure dependencies are published before dependents.
1355
+ * Assigns a "level" to each package based on its depth in the dependency tree.
1356
+ *
1357
+ * This is used by the publish command to publish packages in the correct order.
1358
+ *
1359
+ * @param graph - Dependency graph
1360
+ * @param packagesToPublish - Set of package names to publish
1361
+ * @returns Array of packages in publish order with their dependency level
1362
+ */
1363
+ function getPackagePublishOrder(graph, packagesToPublish) {
1364
+ const result = [];
1365
+ const visited = /* @__PURE__ */ new Set();
1366
+ const toUpdate = new Set(packagesToPublish);
1367
+ const packagesToProcess = new Set(packagesToPublish);
1368
+ for (const pkg of packagesToPublish) {
1369
+ const deps = graph.dependents.get(pkg);
1370
+ if (deps) for (const dep of deps) {
1371
+ packagesToProcess.add(dep);
1372
+ toUpdate.add(dep);
1373
+ }
1374
+ }
1375
+ function visit(pkgName, level) {
1376
+ if (visited.has(pkgName)) return;
1377
+ visited.add(pkgName);
1378
+ const pkg = graph.packages.get(pkgName);
1379
+ if (!pkg) return;
1380
+ const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
1381
+ let maxDepLevel = level;
1382
+ for (const dep of allDeps) if (toUpdate.has(dep)) {
1383
+ visit(dep, level);
1384
+ const depResult = result.find((r) => r.package.name === dep);
1385
+ if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
1386
+ }
1387
+ result.push({
1388
+ package: pkg,
1389
+ level: maxDepLevel
1390
+ });
1391
+ }
1392
+ for (const pkg of toUpdate) visit(pkg, 0);
1393
+ result.sort((a, b) => a.level - b.level);
1394
+ return result;
1395
+ }
1396
+ /**
1284
1397
  * Create version updates for all packages affected by dependency changes
1285
1398
  *
1286
1399
  * When a package is updated, all packages that depend on it should also be updated.
@@ -1741,11 +1854,193 @@ async function prepareWorkflow(options) {
1741
1854
  };
1742
1855
  }
1743
1856
 
1857
+ //#endregion
1858
+ //#region src/core/npm.ts
1859
+ function toNPMError(operation, error, code) {
1860
+ return {
1861
+ type: "npm",
1862
+ operation,
1863
+ message: error instanceof Error ? error.message : String(error),
1864
+ code
1865
+ };
1866
+ }
1867
+ /**
1868
+ * Get the NPM registry URL
1869
+ * Respects NPM_CONFIG_REGISTRY environment variable, defaults to npmjs.org
1870
+ */
1871
+ function getRegistryURL() {
1872
+ return process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
1873
+ }
1874
+ /**
1875
+ * Fetch package metadata from NPM registry
1876
+ * @param packageName - The package name (e.g., "lodash" or "@scope/name")
1877
+ * @returns Result with package metadata or error
1878
+ */
1879
+ async function getPackageMetadata(packageName) {
1880
+ try {
1881
+ const registry = getRegistryURL();
1882
+ const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
1883
+ const response = await fetch(`${registry}/${encodedName}`, { headers: { Accept: "application/json" } });
1884
+ if (!response.ok) {
1885
+ if (response.status === 404) return err(toNPMError("getPackageMetadata", `Package not found: ${packageName}`, "E404"));
1886
+ return err(toNPMError("getPackageMetadata", `HTTP ${response.status}: ${response.statusText}`));
1887
+ }
1888
+ return ok(await response.json());
1889
+ } catch (error) {
1890
+ return err(toNPMError("getPackageMetadata", error, "ENETWORK"));
1891
+ }
1892
+ }
1893
+ /**
1894
+ * Check if a specific package version exists on NPM
1895
+ * @param packageName - The package name
1896
+ * @param version - The version to check (e.g., "1.2.3")
1897
+ * @returns Result with boolean (true if version exists) or error
1898
+ */
1899
+ async function checkVersionExists(packageName, version) {
1900
+ const metadataResult = await getPackageMetadata(packageName);
1901
+ if (!metadataResult.ok) {
1902
+ if (metadataResult.error.code === "E404") return ok(false);
1903
+ return err(metadataResult.error);
1904
+ }
1905
+ return ok(version in metadataResult.value.versions);
1906
+ }
1907
+ /**
1908
+ * Build a package before publishing
1909
+ * @param packageName - The package name to build
1910
+ * @param workspaceRoot - Path to the workspace root
1911
+ * @param options - Normalized release scripts options
1912
+ * @returns Result indicating success or failure
1913
+ */
1914
+ async function buildPackage(packageName, workspaceRoot, options) {
1915
+ if (!options.npm.runBuild) return ok(void 0);
1916
+ try {
1917
+ await runIfNotDry("pnpm", [
1918
+ "--filter",
1919
+ packageName,
1920
+ "build"
1921
+ ], { nodeOptions: {
1922
+ cwd: workspaceRoot,
1923
+ stdio: "inherit"
1924
+ } });
1925
+ return ok(void 0);
1926
+ } catch (error) {
1927
+ return err(toNPMError("buildPackage", error));
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Publish a package to NPM
1932
+ * Uses pnpm to handle workspace protocol and catalog: resolution automatically
1933
+ * @param packageName - The package name to publish
1934
+ * @param workspaceRoot - Path to the workspace root
1935
+ * @param options - Normalized release scripts options
1936
+ * @returns Result indicating success or failure
1937
+ */
1938
+ async function publishPackage(packageName, workspaceRoot, options) {
1939
+ const args = [
1940
+ "--filter",
1941
+ packageName,
1942
+ "publish",
1943
+ "--access",
1944
+ options.npm.access,
1945
+ "--no-git-checks"
1946
+ ];
1947
+ if (options.npm.otp) args.push("--otp", options.npm.otp);
1948
+ if (process.env.NPM_CONFIG_TAG) args.push("--tag", process.env.NPM_CONFIG_TAG);
1949
+ const env = { ...process.env };
1950
+ if (options.npm.provenance) env.NPM_CONFIG_PROVENANCE = "true";
1951
+ try {
1952
+ await runIfNotDry("pnpm", args, { nodeOptions: {
1953
+ cwd: workspaceRoot,
1954
+ stdio: "inherit",
1955
+ env
1956
+ } });
1957
+ return ok(void 0);
1958
+ } catch (error) {
1959
+ const errorMessage = error instanceof Error ? error.message : String(error);
1960
+ return err(toNPMError("publishPackage", error, errorMessage.includes("E403") ? "E403" : errorMessage.includes("EPUBLISHCONFLICT") ? "EPUBLISHCONFLICT" : errorMessage.includes("EOTP") ? "EOTP" : void 0));
1961
+ }
1962
+ }
1963
+
1744
1964
  //#endregion
1745
1965
  //#region src/workflows/publish.ts
1746
1966
  async function publishWorkflow(options) {
1747
- logger.warn("Publish workflow is not implemented yet.");
1748
- logger.verbose("Publish options:", options);
1967
+ logger.section("📦 Publishing Packages");
1968
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1969
+ if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1970
+ const workspacePackages = discovered.value;
1971
+ logger.item(`Found ${workspacePackages.length} packages in workspace`);
1972
+ const graph = buildPackageDependencyGraph(workspacePackages);
1973
+ const publicPackages = workspacePackages.filter((pkg) => !pkg.packageJson.private);
1974
+ logger.item(`Publishing ${publicPackages.length} public packages (private packages excluded)`);
1975
+ if (publicPackages.length === 0) {
1976
+ logger.warn("No public packages to publish");
1977
+ return;
1978
+ }
1979
+ const publishOrder = getPackagePublishOrder(graph, new Set(publicPackages.map((p) => p.name)));
1980
+ const status = {
1981
+ published: [],
1982
+ skipped: [],
1983
+ failed: []
1984
+ };
1985
+ for (const order of publishOrder) {
1986
+ const pkg = order.package;
1987
+ const version = pkg.version;
1988
+ const packageName = pkg.name;
1989
+ logger.section(`📦 ${farver.cyan(packageName)} ${farver.gray(`(level ${order.level})`)}`);
1990
+ logger.step(`Checking if ${farver.cyan(`${packageName}@${version}`)} exists on NPM...`);
1991
+ const existsResult = await checkVersionExists(packageName, version);
1992
+ if (!existsResult.ok) {
1993
+ logger.error(`Failed to check version: ${existsResult.error.message}`);
1994
+ status.failed.push(packageName);
1995
+ exitWithError(`Publishing failed for ${packageName}: ${existsResult.error.message}`, "Check your network connection and NPM registry access");
1996
+ }
1997
+ if (existsResult.value) {
1998
+ logger.info(`Version ${farver.cyan(version)} already exists on NPM, skipping`);
1999
+ status.skipped.push(packageName);
2000
+ continue;
2001
+ }
2002
+ if (options.npm.runBuild) {
2003
+ logger.step(`Building ${farver.cyan(packageName)}...`);
2004
+ const buildResult = await buildPackage(packageName, options.workspaceRoot, options);
2005
+ if (!buildResult.ok) {
2006
+ logger.error(`Failed to build package: ${buildResult.error.message}`);
2007
+ status.failed.push(packageName);
2008
+ exitWithError(`Publishing failed for ${packageName}: build failed`, "Check your build scripts and dependencies");
2009
+ }
2010
+ }
2011
+ logger.step(`Publishing ${farver.cyan(`${packageName}@${version}`)} to NPM...`);
2012
+ const publishResult = await publishPackage(packageName, options.workspaceRoot, options);
2013
+ if (!publishResult.ok) {
2014
+ logger.error(`Failed to publish: ${publishResult.error.message}`);
2015
+ status.failed.push(packageName);
2016
+ let hint;
2017
+ if (publishResult.error.code === "E403") hint = "Authentication failed. Ensure your NPM token or OIDC configuration is correct";
2018
+ else if (publishResult.error.code === "EPUBLISHCONFLICT") hint = "Version conflict. The version may have been published recently";
2019
+ else if (publishResult.error.code === "EOTP") hint = "2FA/OTP required. Provide the otp option or use OIDC authentication";
2020
+ exitWithError(`Publishing failed for ${packageName}`, hint);
2021
+ }
2022
+ logger.success(`Published ${farver.cyan(`${packageName}@${version}`)}`);
2023
+ status.published.push(packageName);
2024
+ logger.step(`Creating git tag ${farver.cyan(`${packageName}@${version}`)}...`);
2025
+ const tagResult = await createAndPushPackageTag(packageName, version, options.workspaceRoot);
2026
+ if (!tagResult.ok) {
2027
+ logger.error(`Failed to create/push tag: ${tagResult.error.message}`);
2028
+ logger.warn(`Package was published but tag was not created. You may need to create it manually.`);
2029
+ } else logger.success(`Created and pushed tag ${farver.cyan(`${packageName}@${version}`)}`);
2030
+ }
2031
+ logger.section("📊 Publishing Summary");
2032
+ logger.item(`${farver.green("✓")} Published: ${status.published.length} package(s)`);
2033
+ if (status.published.length > 0) for (const pkg of status.published) logger.item(` ${farver.green("•")} ${pkg}`);
2034
+ if (status.skipped.length > 0) {
2035
+ logger.item(`${farver.yellow("⚠")} Skipped (already exists): ${status.skipped.length} package(s)`);
2036
+ for (const pkg of status.skipped) logger.item(` ${farver.yellow("•")} ${pkg}`);
2037
+ }
2038
+ if (status.failed.length > 0) {
2039
+ logger.item(`${farver.red("✖")} Failed: ${status.failed.length} package(s)`);
2040
+ for (const pkg of status.failed) logger.item(` ${farver.red("•")} ${pkg}`);
2041
+ }
2042
+ if (status.failed.length > 0) exitWithError(`Publishing completed with ${status.failed.length} failure(s)`);
2043
+ logger.success("All packages published successfully!");
1749
2044
  }
1750
2045
 
1751
2046
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.36",
3
+ "version": "0.1.0-beta.37",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",