@mlaursen/release-script 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mikkel Laursen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @mlaursen/release-script
2
+
3
+ This is the normal npm release script I use. This requires:
4
+
5
+ - [changesets](https://github.com/changesets/changesets) to handle bumping
6
+ versions and generating changelogs.
7
+ - A Github [release token](https://github.com/settings/personal-access-tokens)
8
+ - The token only needs repository access
9
+ - This is normally stored as `GITHUB_TOKEN` in an `.env.local` file that
10
+ should not be committed
11
+
12
+ ## Installation
13
+
14
+ The release script relies on
15
+ [changesets](https://github.com/changesets/changesets) to handle bumping
16
+ versions and generating changelogs.
17
+
18
+ ```sh
19
+ pnpm install --save-dev @mlaursen/release-script \
20
+ @changesets/cli \
21
+ tsx
22
+ ```
23
+
24
+ Setup the `.changeset` dir if needed:
25
+
26
+ ```sh
27
+ pnpm changeset init
28
+ git add .changeset
29
+ git add -u
30
+ git commit -m "build: setup changesets"
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ Create a `scripts/release.ts` file with:
36
+
37
+ ```ts
38
+ import { release } from "@mlaursen/release-script";
39
+
40
+ await release({
41
+ repo: "{{REPO_NAME}}", // i.e. eslint-config
42
+
43
+ // if the repo is not under `mlaursen` for some reason
44
+ // owner: "mlaursen",
45
+
46
+ // If there is a custom clean command for releases. `clean` is the default
47
+ // cleanCommand: "clean",
48
+
49
+ // If there is a custom build command for releases. `build` is the default
50
+ // buildCommand: "build",
51
+
52
+ // An optional flag if the build step should be skipped. `false` by default
53
+ // skipBuild: process.argv.includes("--skip-build"),
54
+
55
+ // This is useful for monorepos where only a single Github release needs to
56
+ // be created. Defaults to `JSON.parse(await readFile("package.json)).name`
57
+ // mainPackage: "{{PACKAGE_NAME}}",
58
+
59
+ // If the version message needs to be customized. The following is the default
60
+ // versionMessage: "build(version): version package",
61
+
62
+ // An optional `.env` file path that includes the `GITLAB_TOKEN` environment variable.
63
+ // envPath: ".env.local",
64
+ });
65
+ ```
66
+
67
+ Next, update `package.json` to include the release script:
68
+
69
+ ```diff
70
+ "scripts": {
71
+ "prepare": "husky",
72
+ "typecheck": "tsc --noEmit",
73
+ "check-format": "prettier --check .",
74
+ "format": "prettier --write .",
75
+ "clean": "rm -rf dist",
76
+ "build": "tsc -p tsconfig.json",
77
+ + "release": "tsx index.ts"
78
+ },
79
+ ```
80
+
81
+ Finally, run the release script whenever a new release should go out:
82
+
83
+ ```sh
84
+ pnpm release
85
+ ```
@@ -0,0 +1 @@
1
+ export declare function continueRelease(): Promise<void>;
@@ -0,0 +1,7 @@
1
+ import confirm from "@inquirer/confirm";
2
+ export async function continueRelease() {
3
+ const confirmed = confirm({ message: "Continue the release?" });
4
+ if (!confirmed) {
5
+ process.exit(1);
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ export interface ConfigurableCreateReleaseOptions {
2
+ repo: string;
3
+ /**
4
+ * @defaultValue `"mlaursen"`
5
+ */
6
+ owner?: string;
7
+ /**
8
+ * The `.env` file to load to get the `GITHUB_TOKEN` environment variable.
9
+ *
10
+ * @defaultValue `".env.local"`
11
+ */
12
+ envPath?: string;
13
+ }
14
+ export interface CreateReleaseOptions extends ConfigurableCreateReleaseOptions {
15
+ body: string;
16
+ version: string;
17
+ override?: boolean;
18
+ tagPrefix: string;
19
+ prerelease: boolean;
20
+ }
21
+ export declare function createRelease(options: CreateReleaseOptions): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import confirm from "@inquirer/confirm";
2
+ import { Octokit } from "@octokit/core";
3
+ import dotenv from "dotenv";
4
+ export async function createRelease(options) {
5
+ const { version, body, tagPrefix, override, owner = "mlaursen", repo, prerelease, envPath = ".env.local", } = options;
6
+ dotenv.config({ path: envPath, override });
7
+ const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
8
+ try {
9
+ const response = await octokit.request("POST /repos/{owner}/{repo}/releases", {
10
+ owner,
11
+ repo,
12
+ tag_name: `${tagPrefix}@${version}`,
13
+ body,
14
+ prerelease,
15
+ });
16
+ console.log(`Created release: ${response.data.html_url}`);
17
+ }
18
+ catch (e) {
19
+ console.error(e);
20
+ console.log();
21
+ console.log("The npm token is most likely expired or never created. Update the `.env.local` to include the latest GITHUB_TOKEN");
22
+ console.log("Regenerate the token: https://github.com/settings/personal-access-tokens");
23
+ if (!(await confirm({ message: "Try creating the Github release again?" }))) {
24
+ process.exit(1);
25
+ }
26
+ return createRelease({ ...options, override: true });
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export declare function getCurrentChangeset(): Promise<string>;
@@ -0,0 +1,26 @@
1
+ import confirm from "@inquirer/confirm";
2
+ import rawlist from "@inquirer/rawlist";
3
+ import { execSync } from "node:child_process";
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ export async function getCurrentChangeset() {
7
+ let changesetName = execSync("git diff --name-only @{upstream} .changeset/*.md")
8
+ .toString()
9
+ .trim();
10
+ if (!changesetName ||
11
+ !(await confirm({
12
+ message: `Is "${changesetName}" the correct changeset path?`,
13
+ }))) {
14
+ const changesetNames = await readdir(".changeset");
15
+ changesetName = await rawlist({
16
+ message: "Select the changeset path",
17
+ choices: changesetNames
18
+ .filter((changeset) => changeset.endsWith(".md"))
19
+ .map((changeset) => ({
20
+ value: changeset,
21
+ })),
22
+ });
23
+ changesetName = join(".changeset", changesetName);
24
+ }
25
+ return await readFile(changesetName, "utf8");
26
+ }
@@ -0,0 +1,2 @@
1
+ export type PackageManager = "npm" | "yarn" | "pnpm";
2
+ export declare function getPackageManager(): Promise<PackageManager>;
@@ -0,0 +1,24 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ export async function getPackageManager() {
4
+ const rawPackageJson = await readFile(resolve(process.cwd(), "package.json"), "utf8");
5
+ const packageJson = JSON.parse(rawPackageJson);
6
+ if (typeof packageJson.volta === "object" && packageJson.volta) {
7
+ const { volta } = packageJson;
8
+ if ("pnpm" in volta) {
9
+ return "pnpm";
10
+ }
11
+ if ("yarn" in volta) {
12
+ return "yarn";
13
+ }
14
+ return "npm";
15
+ }
16
+ if (typeof packageJson.packageManager === "string") {
17
+ const mgr = packageJson.packageManagerreplace(/@.+/, "");
18
+ if (mgr === "pnpm" || mgr === "yarn" || mgr === "npm") {
19
+ return mgr;
20
+ }
21
+ throw new Error(`Unsupported package mananger "${mgr}" in package.json`);
22
+ }
23
+ throw new Error("Unable to find a package manager");
24
+ }
@@ -0,0 +1 @@
1
+ export declare function getReleaseVersion(mainPackage?: string): Promise<string>;
@@ -0,0 +1,21 @@
1
+ import confirm from "@inquirer/confirm";
2
+ import input from "@inquirer/input";
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ export async function getReleaseVersion(mainPackage) {
6
+ let packageJsonPath = "package.json";
7
+ if (mainPackage) {
8
+ packageJsonPath = `packages/${mainPackage}.json`;
9
+ }
10
+ packageJsonPath = resolve(process.cwd(), packageJsonPath);
11
+ const packageJson = await readFile(packageJsonPath, "utf8");
12
+ const { version } = JSON.parse(packageJson);
13
+ if (await confirm({
14
+ message: `Is "${version}" the next github release version?`,
15
+ })) {
16
+ return version;
17
+ }
18
+ return await input({
19
+ message: "Input the next release version for Github",
20
+ });
21
+ }
@@ -0,0 +1 @@
1
+ export declare function getTagPrefix(mainPackage?: string): Promise<string>;
@@ -0,0 +1,24 @@
1
+ import confirm from "@inquirer/confirm";
2
+ import input from "@inquirer/input";
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ async function getPkgName() {
6
+ const pkgJson = await readFile(resolve(process.cwd(), "package.json"), "utf8");
7
+ return JSON.parse(pkgJson).name || "";
8
+ }
9
+ export async function getTagPrefix(mainPackage) {
10
+ let pkg = "";
11
+ try {
12
+ pkg = mainPackage ?? (await getPkgName());
13
+ }
14
+ catch {
15
+ console.error("Unable to get package name from package.json");
16
+ }
17
+ if (!pkg ||
18
+ !(await confirm({ message: `Use ${pkg} as the next tag name?` }))) {
19
+ return await input({
20
+ message: "Enter the next tag name prefix",
21
+ });
22
+ }
23
+ process.exit(1);
24
+ }
@@ -0,0 +1,9 @@
1
+ import { ConfigurableCreateReleaseOptions } from "./createRelease.js";
2
+ export interface ReleaseOptions extends ConfigurableCreateReleaseOptions {
3
+ skipBuild?: boolean;
4
+ cleanCommand?: string;
5
+ buildCommand?: string;
6
+ mainPackage?: string;
7
+ versionMessage?: string;
8
+ }
9
+ export declare function release(options: ReleaseOptions): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { execSync } from "node:child_process";
2
+ import { continueRelease } from "./continueRelease.js";
3
+ import { getCurrentChangeset } from "./getCurrentChangeset.js";
4
+ import { getPackageManager } from "./getPackageManager.js";
5
+ import { getReleaseVersion } from "./getReleaseVersion.js";
6
+ import { createRelease, } from "./createRelease.js";
7
+ import { getTagPrefix } from "./getTagPrefix.js";
8
+ const exec = (command, opts) => {
9
+ console.log(command);
10
+ execSync(command, opts);
11
+ };
12
+ export async function release(options) {
13
+ const { owner, repo, envPath, skipBuild, cleanCommand = "clean", buildCommand = "build", mainPackage, versionMessage = "build(version): version package", } = options;
14
+ const pkgManager = await getPackageManager();
15
+ if (!skipBuild) {
16
+ exec(`${pkgManager} ${cleanCommand}`);
17
+ exec(`${pkgManager} ${buildCommand}`);
18
+ }
19
+ exec(`${pkgManager} changeset`, { stdio: "inherit" });
20
+ await continueRelease();
21
+ exec("git add -u");
22
+ exec("git add .changeset");
23
+ const changeset = await getCurrentChangeset();
24
+ exec("pnpm changeset version", { stdio: "inherit" });
25
+ exec("git add -u");
26
+ const version = await getReleaseVersion(mainPackage);
27
+ await continueRelease();
28
+ exec(`git commit -m "${versionMessage}"`);
29
+ exec(`${pkgManager} changeset publish`, { stdio: "inherit" });
30
+ exec("git push --follow-tags");
31
+ await createRelease({
32
+ owner,
33
+ repo,
34
+ body: changeset,
35
+ tagPrefix: await getTagPrefix(mainPackage),
36
+ version,
37
+ envPath,
38
+ prerelease: version.includes("next"),
39
+ });
40
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mlaursen/release-script",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "The release script I normally use for packages I publish to npm",
6
+ "repository": "https://github.com/mlaursen/release-script.git",
7
+ "author": "Mikkel Laursen <mlaursen03@gmail.com>",
8
+ "license": "MIT",
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/mlaursen/release-script/issues"
18
+ },
19
+ "keywords": [
20
+ "release",
21
+ "script",
22
+ "npm"
23
+ ],
24
+ "dependencies": {
25
+ "@changesets/cli": "^2.29.5",
26
+ "@inquirer/confirm": "^5.1.14",
27
+ "@inquirer/input": "^4.2.1",
28
+ "@inquirer/rawlist": "^4.1.5",
29
+ "@octokit/core": "^7.0.3",
30
+ "dotenv": "^17.2.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.15.29",
34
+ "husky": "^9.1.7",
35
+ "lint-staged": "^16.1.2",
36
+ "prettier": "^3.6.2",
37
+ "tsx": "^4.20.3",
38
+ "typescript": "^5.8.3"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "lint-staged": {
44
+ "**/*.{js,jsx,ts,tsx,md,yml,yaml,json}": [
45
+ "prettier --write"
46
+ ]
47
+ },
48
+ "volta": {
49
+ "node": "22.17.1",
50
+ "pnpm": "10.13.1"
51
+ },
52
+ "scripts": {
53
+ "typecheck": "tsc --noEmit",
54
+ "check-format": "prettier --check .",
55
+ "format": "prettier --write .",
56
+ "clean": "rm -rf dist",
57
+ "build": "tsc -p tsconfig.build.json",
58
+ "release": "tsx index.ts"
59
+ }
60
+ }