diff --git a/.github/workflows/PublishNpm.yml b/.github/workflows/PublishNpm.yml index e3fe625c..9091323b 100644 --- a/.github/workflows/PublishNpm.yml +++ b/.github/workflows/PublishNpm.yml @@ -77,23 +77,11 @@ jobs: with: node-version: '24' # Includes a version of npm higher than 11.5.1, which is needed for OIDC - - name: Release Tag - run: | - package="$(echo "${{matrix.release.package}}" | sed -E '/^test$/! s/^/@mendix\//')" # Prefix all packages except test - release="${{ matrix.release.version }}" - latest="$(npm view $package dist-tags.latest)" - older="$(npx semver@7.8.5 "$latest" "$release" 2>/dev/null | head -n 1)" - tag="latest" - if [[ "$latest" != "$older" ]]; then - tag="$(echo "$release" | sed -E 's/^([0-9]+\.[0-9]+).[0-9]+$/\1-latest/')" - fi - printf 'Using release tag %s' "$tag" - printf 'release_tag=%s' "$tag" >> "$GITHUB_ENV" - - name: Publish package - working-directory: run: | - cmd="npm publish --tag $release_tag ./packages/${{ matrix.release.package }}/mendix-${{ matrix.release.package }}-${{matrix.release.version }}.tgz" + dist_tag=$(./packages/release-tools/releaseTags.ts @mendix/${{ matrix.release.package }} ${{ matrix.release.version }}) + + cmd="npm publish --tag $dist_tag ./packages/${{ matrix.release.package }}/mendix-${{ matrix.release.package }}-${{matrix.release.version }}.tgz" if [[ "${{ matrix.release.package }}" == "test" ]]; then echo "$cmd" else diff --git a/packages/release-tools/createRelease.ts b/packages/release-tools/createRelease.ts index 15307dfd..a55233ce 100755 --- a/packages/release-tools/createRelease.ts +++ b/packages/release-tools/createRelease.ts @@ -6,10 +6,12 @@ import { getPackageInfo, gh, type PackageInfo, - ChangelogFileWrapper + ChangelogFileWrapper, + parseArgument } from "./utils/index.ts"; -import { ArgumentError, handleError } from "./utils/errors.ts"; +import { handleError } from "./utils/errors.ts"; import { format } from "node:util"; +import { ensure } from "./utils/assertions.ts"; const packages = new Map([ [ @@ -40,13 +42,15 @@ main().catch(e => { }); async function main(): Promise { - const arg = process.argv[2]; - const mendixPackage = packages.get(arg); - if (!mendixPackage) - throw new ArgumentError(format("package name of (%s)", packages.keys().toArray().join(", ")), arg); - const branch = process.argv[3]; - if (!branch || !/^(master|version\/.*)$/.test(branch)) - throw new ArgumentError("branch name of (master, version/*)", branch); + const mendixPackage = parseArgument("package name", 2, input => + ensure(packages.get(input), "Unknown package, must be one of " + packages.keys().toArray().join(", ")) + ); + const branch = parseArgument("branch name", 3, input => { + if (/^(master|version\/.*)$/.test(input)) { + return input; + } + throw new Error("Must match master, version/*"); + }); const dirname = new URL("./", import.meta.url).pathname; const pwtPath = join(dirname, "../", mendixPackage.name); diff --git a/packages/release-tools/package.json b/packages/release-tools/package.json index 7ac8c119..737625a3 100644 --- a/packages/release-tools/package.json +++ b/packages/release-tools/package.json @@ -6,8 +6,9 @@ "main": "./createRelease.ts", "type": "module", "bin": { + "changelog-parser": "./utils/changelog-parser/cli.ts", "create-release": "./createRelease.ts", - "changelog-parser": "./utils/changelog-parser/cli.ts" + "release-tags": "./releaseTags.ts" }, "scripts": { "release:pwt": "./createRelease.ts pluggable-widgets-tools", @@ -23,11 +24,13 @@ "@tsconfig/node22": "^22.0.5", "@types/node": "^22.18.0", "@types/node-fetch": "2.6.13", + "@types/semver": "^7.7.1", "peggy": "^5.1.0", "typescript": "^5.9.3" }, "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "semver": "^7.8.5" }, "keywords": [], "author": "", diff --git a/packages/release-tools/releaseTags.ts b/packages/release-tools/releaseTags.ts new file mode 100755 index 00000000..f00cec10 --- /dev/null +++ b/packages/release-tools/releaseTags.ts @@ -0,0 +1,36 @@ +#! /usr/bin/env -S node --experimental-strip-types +import { exec } from "node:child_process"; +import { format, promisify } from "node:util"; +import { parse, gt } from "semver"; +import { handleError } from "./utils/errors.ts"; +import { parseArgument } from "./utils/shell.ts"; +import { ensure } from "./utils/assertions.ts"; + +const RELEASEABLE_PACKAGES = ["@mendix/pluggable-widgets-tools", "@mendix/generator-widget"] as const; + +async function main() { + const releasePackage = parseArgument("package name", 2, input => + ensure( + RELEASEABLE_PACKAGES.find(p => p === input), + "Unknown package, must be one of " + RELEASEABLE_PACKAGES.join(", ") + ) + ); + const releaseVersion = parseArgument("release version", 3, input => + ensure(parse(input), "Must use the semver format") + ); + + const { stdout } = await promisify(exec)(`npm show ${releasePackage} dist-tags.latest`); + const latest = parse(stdout.trim()); + if (!latest) { + throw Error(`Unable to parse latest released version "${stdout}"`); + } + + const tag = gt(releaseVersion, latest) ? "latest" : `${releaseVersion.major}.${releaseVersion.minor}-latest`; + + console.log(tag); +} + +await main().catch(e => { + handleError(e, format("\nUsage: %s ", process.argv[1])); + process.exit(1); +}); diff --git a/packages/release-tools/utils/assertions.ts b/packages/release-tools/utils/assertions.ts new file mode 100644 index 00000000..64350f14 --- /dev/null +++ b/packages/release-tools/utils/assertions.ts @@ -0,0 +1,6 @@ +export function ensure(input: T | undefined | null, message?: string): T { + if (input === undefined || input === null) { + throw Error(message ?? `Expected a value but received ${input}`); + } + return input; +} diff --git a/packages/release-tools/utils/errors.ts b/packages/release-tools/utils/errors.ts index 1af6b426..27b5a3ba 100644 --- a/packages/release-tools/utils/errors.ts +++ b/packages/release-tools/utils/errors.ts @@ -9,8 +9,8 @@ export class UsageError extends Error { export class ArgumentError extends UsageError { // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(argument: string, received: any) { - super(`Argument "${received}" is not a valid ${argument}`); + constructor(argument: string, received: any, reason?: string) { + super(`Argument "${received}" is not a valid ${argument}${reason ? `: ${reason}` : ""}`); } } diff --git a/packages/release-tools/utils/shell.ts b/packages/release-tools/utils/shell.ts index 9ccd1ae5..9777f6ed 100644 --- a/packages/release-tools/utils/shell.ts +++ b/packages/release-tools/utils/shell.ts @@ -1,4 +1,6 @@ import { exec } from "node:child_process"; +import { ArgumentError } from "./errors.ts"; +import { argv } from "node:process"; export function execShellCommand(cmd: string | string[], workingDirectory: string = process.cwd()): Promise { const command = Array.isArray(cmd) ? cmd.join(" && ") : cmd; @@ -16,3 +18,22 @@ export function execShellCommand(cmd: string | string[], workingDirectory: strin }); }); } + +export function parseArgument(name: string, position: number, parser: (input: string) => T) { + const value = argv[position]; + if (!value) { + throw new ArgumentError(name, value, "Cannot be empty"); + } + try { + return parser(value); + } catch (e) { + if (e instanceof Error) { + throw new ArgumentError(name, value, e.message); + } + if (typeof e === "string") { + throw new ArgumentError(name, value, e); + } + console.error(e); + throw new ArgumentError(name, value); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a186eaf1..6df927e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,6 +409,9 @@ importers: node-fetch: specifier: ^2.7.0 version: 2.7.0 + semver: + specifier: ^7.8.5 + version: 7.8.5 devDependencies: '@tsconfig/node-ts': specifier: ^23.6.4 @@ -422,6 +425,9 @@ importers: '@types/node-fetch': specifier: 2.6.13 version: 2.6.13 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 peggy: specifier: ^5.1.0 version: 5.1.0 @@ -8898,7 +8904,7 @@ snapshots: proggy: 4.0.0 promise-all-reject-late: 1.0.1 promise-call-limit: 3.0.2 - semver: 7.8.0 + semver: 7.8.5 ssri: 13.0.1 treeverse: 3.0.0 walk-up-path: 4.0.0 @@ -8907,7 +8913,7 @@ snapshots: '@npmcli/fs@5.0.0': dependencies: - semver: 7.8.0 + semver: 7.8.5 '@npmcli/git@7.0.2': dependencies: @@ -8917,7 +8923,7 @@ snapshots: lru-cache: 11.3.6 npm-pick-manifest: 11.0.3 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 which: 6.0.1 '@npmcli/installed-package-contents@4.0.0': @@ -8938,7 +8944,7 @@ snapshots: json-parse-even-better-errors: 5.0.0 pacote: 21.5.0 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -8953,7 +8959,7 @@ snapshots: hosted-git-info: 9.0.3 json-parse-even-better-errors: 5.0.0 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 spdx-expression-parse: 4.0.0 '@npmcli/promise-spawn@9.0.1': @@ -9193,7 +9199,7 @@ snapshots: debug: 4.4.3 invariant: 2.2.4 metro: 0.87.1 - semver: 7.8.0 + semver: 7.8.5 transitivePeerDependencies: - bufferutil - supports-color @@ -9855,7 +9861,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.3 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.5 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -11890,7 +11896,7 @@ snapshots: '@babel/parser': 7.29.3 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 - semver: 7.8.0 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -12530,7 +12536,7 @@ snapshots: jest-message-util: 30.4.1 jest-util: 30.4.1 pretty-format: 30.4.1 - semver: 7.8.0 + semver: 7.8.5 synckit: 0.11.12 transitivePeerDependencies: - supports-color @@ -12556,7 +12562,7 @@ snapshots: jest-message-util: 30.5.1 jest-util: 30.5.1 pretty-format: 30.5.1 - semver: 7.8.0 + semver: 7.8.5 synckit: 0.11.12 transitivePeerDependencies: - supports-color @@ -12879,7 +12885,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.5 make-dir@5.1.0: {} @@ -13265,7 +13271,7 @@ snapshots: graceful-fs: 4.2.11 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 tar: 7.5.15 tinyglobby: 0.2.16 undici: 6.25.0 @@ -13282,7 +13288,7 @@ snapshots: normalize-package-data@8.0.0: dependencies: hosted-git-info: 9.0.3 - semver: 7.8.0 + semver: 7.8.5 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -13295,7 +13301,7 @@ snapshots: npm-install-checks@8.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.5 npm-normalize-package-bin@5.0.0: {} @@ -13303,7 +13309,7 @@ snapshots: dependencies: hosted-git-info: 9.0.3 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 validate-npm-package-name: 7.0.2 npm-packlist@10.0.4: @@ -13316,7 +13322,7 @@ snapshots: npm-install-checks: 8.0.0 npm-normalize-package-bin: 5.0.0 npm-package-arg: 13.0.2 - semver: 7.8.0 + semver: 7.8.5 npm-registry-fetch@19.1.1: dependencies: @@ -13511,7 +13517,7 @@ snapshots: ky: 1.14.3 registry-auth-token: 5.1.1 registry-url: 6.0.1 - semver: 7.8.0 + semver: 7.8.5 package-name-regex@2.0.6: {}