diff --git a/.github/workflows/cli-release-process.yml b/.github/workflows/cli-release-process.yml index cc4930befaa9..428ca47018d1 100644 --- a/.github/workflows/cli-release-process.yml +++ b/.github/workflows/cli-release-process.yml @@ -19,7 +19,10 @@ env: JAVA_DISTRO: temurin NEXT_VERSION: '1.0.0-SNAPSHOT' GRAALVM_VERSION: '22.1.0' - PACKAGE_TYPE: 'uber-jar' + MVN_PACKAGE_TYPE: 'uber-jar' + NPM_PACKAGE_NAME: 'dotcli' + MVN_PACKAGE_NAME: 'dotcms-cli' + NODE_VERSION: 19 jobs: precheck: @@ -75,7 +78,7 @@ jobs: ./mvnw -B -ntp versions:set versions:commit -DnewVersion=$RELEASE_VERSION - git commit --allow-empty -a -m "🏁 Releasing version $RELEASE_VERSION" + git commit --allow-empty -a -m "🏁 Releasing CLI version $RELEASE_VERSION" git push https://${{ secrets.CI_MACHINE_USER }}:${{ secrets.CI_MACHINE_TOKEN }}@github.com/${GITHUB_REPOSITORY} echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" @@ -89,7 +92,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ macos-13-xlarge, macOS-latest, ubuntu-latest, windows-latest ] # + os: [ macos-13-xlarge, macOS-latest, ubuntu-latest ] runs-on: ${{ matrix.os }} steps: @@ -104,12 +107,12 @@ jobs: run: | if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then echo "GraalVM on Linux (AMD64)" - + ARCH=amd64 PLATFORM=linux INSTALLATION_PATH=/usr/lib/jvm - - else + + else if [ "${{ matrix.os }}" == "macos-13-xlarge" ]; then echo "GraalVM on Mac (AARCH64)" ARCH=aarch64 @@ -117,27 +120,27 @@ jobs: echo "GraalVM on Mac (AMD64)" ARCH=amd64 fi - + PLATFORM=darwin - INSTALLATION_PATH=/Library/Java/JavaVirtualMachines - fi - + INSTALLATION_PATH=/Library/Java/JavaVirtualMachines + fi + echo "PLATFORM=$PLATFORM" echo "ARCH=$ARCH" echo "INSTALLATION_PATH=$INSTALLATION_PATH" - + wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${{ env.GRAALVM_VERSION }}/graalvm-ce-java11-${PLATFORM}-${ARCH}-${{ env.GRAALVM_VERSION }}.tar.gz sudo mkdir -p $INSTALLATION_PATH - tar -xzf graalvm-ce-java11-${PLATFORM}-${ARCH}-${{ env.GRAALVM_VERSION }}.tar.gz + tar -xzf graalvm-ce-java11-${PLATFORM}-${ARCH}-${{ env.GRAALVM_VERSION }}.tar.gz sudo mv graalvm-ce-java11-${{ env.GRAALVM_VERSION }} $INSTALLATION_PATH - + if [ "${{ matrix.os }}" != "ubuntu-latest" ]; then - sudo xattr -r -d com.apple.quarantine /Library/Java/JavaVirtualMachines/graalvm-ce-java11-${{ env.GRAALVM_VERSION }}/Contents/Home + sudo xattr -r -d com.apple.quarantine /Library/Java/JavaVirtualMachines/graalvm-ce-java11-${{ env.GRAALVM_VERSION }}/Contents/Home GRAALVM_HOME="${INSTALLATION_PATH}/graalvm-ce-java11-${{ env.GRAALVM_VERSION }}/Contents/Home" else GRAALVM_HOME="${INSTALLATION_PATH}/graalvm-ce-java11-${{ env.GRAALVM_VERSION }}" fi - + echo "GRAALVM_HOME=$GRAALVM_HOME" >> $GITHUB_ENV echo "JAVA_HOME=$GRAALVM_HOME" >> $GITHUB_ENV PATH="$GRAALVM_HOME/bin:$PATH" @@ -170,7 +173,7 @@ jobs: echo "JAVA_HOME=${env:JAVA_HOME}" >> "$env:GITHUB_ENV" echo "Path=${env:Path}" >> "$env:GITHUB_ENV" - gu.cmd install native-image + gu.cmd install native-image - name: 'Cache Maven packages' uses: actions/cache@v4 @@ -196,7 +199,7 @@ jobs: - name: 'Build uber-jar' working-directory: ${{ github.workspace }} run: | - ./mvnw package -Dquarkus.package.type=${{ env.PACKAGE_TYPE }} -DskipTests=${{ github.event.inputs.skipTests }} -pl :dotcms-cli + ./mvnw package -Dquarkus.package.type=${{ env.MVN_PACKAGE_TYPE }} -DskipTests=${{ github.event.inputs.skipTests }} -pl :dotcms-cli - name: 'Build Native Image (Linux/MacOS)' if: ${{ matrix.os != 'windows-latest' }} @@ -228,7 +231,7 @@ jobs: path: | ${{ github.workspace }}/tools/dotcms-cli/cli/target/*-runner.jar ${{ github.workspace }}/tools/dotcms-cli/cli/target/distributions/*.zip - ${{ github.workspace }}/tools/dotcms-cli/cli/target/distributions/*.tar.gz + ${{ github.workspace }}/tools/dotcms-cli/cli/target/distributions/*.tar.gz release: needs: [ precheck, build ] @@ -249,10 +252,10 @@ jobs: - name: 'Download all build artifacts' uses: actions/download-artifact@v4 with: - pattern: artifacts-* path: ${{ github.workspace }}/artifacts + pattern: artifacts-* merge-multiple: true - + - name: 'List artifacts' run: | ls -R @@ -296,5 +299,95 @@ jobs: ./mvnw -B -ntp versions:set versions:commit -DnewVersion=$NEXT_VERSION - git commit --allow-empty -a -m "⬆️ Next version $NEXT_VERSION" + git commit --allow-empty -a -m "⬆️ Next CLI version $NEXT_VERSION" git push https://${{ secrets.CI_MACHINE_USER }}:${{ secrets.CI_MACHINE_TOKEN }}@github.com/${GITHUB_REPOSITORY} + + publish-npm-package: + name: "Publish NPM Package" + if: success() + needs: [ build, release ] + runs-on: ubuntu-latest + steps: + - name: 'Checkout code' + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: 'Set up Node.js' + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: 'Install Jinja2' + run: pip install jinja2-cli + + - name: 'Download all build artifacts' + uses: actions/download-artifact@v4 + with: + path: ${{ github.workspace }}/artifacts + pattern: artifacts-* + merge-multiple: true + + - name: 'Generate package version suffix' + id: generate-version-suffix + env: + MVN_PACKAGE_VERSION: ${{ github.event.inputs.version }} + IS_SNAPSHOT_VERSION: ${{ contains(github.event.inputs.version, 'SNAPSHOT') }} + run: | + if [ "$IS_SNAPSHOT_VERSION" = "true" ]; then + echo "Snapshot version found."; + TAG="rc" + if npm view $NPM_PACKAGE_NAME &> /dev/null; then + echo "${NPM_PACKAGE_NAME} found."; + LAST_RC_VERSION=$(npm view $NPM_PACKAGE_NAME versions --json | jq -r 'map(select(test("-rc\\d+$"))) | max') + echo $LAST_RC_VERSION + NEXT_RC_VERSION=$(echo "$LAST_RC_VERSION" | awk -F '-rc' '{print $1 "-rc" $2 + 1}') + echo $NEXT_RC_VERSION + RC_SUFFIX=$(echo "$NEXT_RC_VERSION" | sed -n 's/.*-rc\([0-9]*\)/-rc\1/p') + echo $RC_SUFFIX + else + echo "${NPM_PACKAGE_NAME} not found."; + RC_SUFFIX="-rc1" + fi; + else + echo "Release version found."; + TAG="latest" + RC_SUFFIX="" + fi; + + VERSION=$(echo "$MVN_PACKAGE_VERSION" | sed 's/-SNAPSHOT//I') + echo "NPM_PACKAGE_VERSION=${VERSION}${RC_SUFFIX}" >> $GITHUB_ENV + echo "NPM_PACKAGE_VERSION_TAG=$TAG" >> $GITHUB_ENV + + - name: 'NPM Package setup' + working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm/ + env: + MVN_PACKAGE_VERSION: ${{ github.event.inputs.version }} + run: | + echo "Adding bin folder with all the binaries" + mkdir -p bin + find ${{ github.workspace }}/artifacts/distributions/ -name "*.zip" -exec unzip -d bin {} \; + + echo "Adding wrapper script" + mv src/postinstall.js.seed src/postinstall.js + + echo "Adding README.md file" + cp ${{ github.workspace }}/tools/dotcms-cli/README.md . + + echo "Adding package.json file" + jinja2 package.j2 -D packageName=${MVN_PACKAGE_NAME} -D npmPackageName=${NPM_PACKAGE_NAME} -D npmPackageVersion=${NPM_PACKAGE_VERSION} -D packageVersion=${MVN_PACKAGE_VERSION} --format json -o package.json + rm -f package.j2 + + cat package.json + cat src/postinstall.js + + - name: 'NPM Package tree' + run: ls -R ${{ github.workspace }}/tools/dotcms-cli/npm/ + + - name: 'Publish to NPM registry' + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc + npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} diff --git a/tools/dotcms-cli/cli/src/assembly/assembly.xml b/tools/dotcms-cli/cli/src/assembly/assembly.xml index b00cd3c07e6f..2a29c787dfdc 100644 --- a/tools/dotcms-cli/cli/src/assembly/assembly.xml +++ b/tools/dotcms-cli/cli/src/assembly/assembly.xml @@ -25,19 +25,15 @@ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd"> dist - tar.gz + zip - dir + false - - - - ${project.build.directory}/${project.artifactId}-${project.version}-runner${executable-suffix} - ./bin - ${project.artifactId}${executable-suffix} + ./ + ${project.artifactId}-${project.version}-${os.detected.classifier}${executable-suffix} 0755 diff --git a/tools/dotcms-cli/jreleaser.yml b/tools/dotcms-cli/jreleaser.yml index 82124d919849..f1fa50337164 100644 --- a/tools/dotcms-cli/jreleaser.yml +++ b/tools/dotcms-cli/jreleaser.yml @@ -56,10 +56,10 @@ distributions: platform: 'osx-aarch_64' - path: '{{artifactsDir}}/distributions/dotcms-cli-{{projectVersion}}-osx-x86_64.zip' platform: 'osx-x86_64' - - path: '{{artifactsDir}}/distributions/dotcms-cli-{{projectVersion}}-linux-x86_64.tar.gz' + - path: '{{artifactsDir}}/distributions/dotcms-cli-{{projectVersion}}-linux-x86_64.zip' platform: 'linux-x86_64' - - path: '{{artifactsDir}}/distributions/dotcms-cli-{{projectVersion}}-windows-x86_64.zip' - platform: 'windows-x86_64' +# - path: '{{artifactsDir}}/distributions/dotcms-cli-{{projectVersion}}-windows-x86_64.zip' +# platform: 'windows-x86_64' upload: artifactory: diff --git a/tools/dotcms-cli/npm/package.j2 b/tools/dotcms-cli/npm/package.j2 new file mode 100644 index 000000000000..ed03f5a1b797 --- /dev/null +++ b/tools/dotcms-cli/npm/package.j2 @@ -0,0 +1,37 @@ +{ + "name": "@dotcms/{{ npmPackageName }}", + "version": "{{ npmPackageVersion }}", + "scripts": { + "postinstall": "node src/postinstall.js install", + "postuninstall": "node src/postinstall.js uninstall && npm prune" + }, + "binaries": { + "{{ packageName }}-darwin-arm64": "bin/{{ packageName }}-{{ packageVersion }}-osx-aarch_64", + "{{ packageName }}-darwin-x64": "bin/{{ packageName }}-{{ packageVersion }}-osx-x86_64", + "{{ packageName }}-linux-x64": "bin/{{ packageName }}-{{ packageVersion }}-linux-x86_64" + }, + "alias": "{{ npmPackageName }}", + "packageName": "{{ packageName }}", + "files": [ + "bin", + "src" + ], + "description": "Official command-line tool to manage dotCMS content.", + "repository": { + "type": "git", + "url": "git+https://github.com/dotCMS/core.git#master" + }, + "keywords": [ + "dotCMS", + "CMS", + "Content Management", + "CLI", + "dotCMS CLI", + "dotCMS command-line tool" + ], + "author": "dotcms ", + "license": "MIT", + "bugs": { + "url": "https://github.com/dotCMS/core/issues" + } +} diff --git a/tools/dotcms-cli/npm/src/postinstall.js.seed b/tools/dotcms-cli/npm/src/postinstall.js.seed new file mode 100644 index 000000000000..041077aa7b62 --- /dev/null +++ b/tools/dotcms-cli/npm/src/postinstall.js.seed @@ -0,0 +1,154 @@ +"use strict"; + +const path = require('path'); +const fs = require('fs').promises; +const os = require('os'); + +const ARCHITECTURE_MAPPING = { + "x64": "x86_64", + "arm64": "aarch_64" +}; + +const PLATFORM_MAPPING = { + "darwin": "osx", + "linux": "linux" +}; + +function getGlobalBinPath() { + const npmGlobalPrefix = process.env.PREFIX || process.env.npm_config_prefix || process.env.HOME; + return path.join(npmGlobalPrefix, 'bin'); +} + +function validatePackageConfig(packageJson) { + if (!packageJson.version || !packageJson.packageName || !packageJson.alias || !packageJson.binaries || typeof packageJson.binaries !== "object") { + throw new Error("Invalid package.json. 'version', 'packageName', 'alias' and 'binaries' must be specified."); + } +} + +async function parsePackageJson() { + + console.log("Installing CLI"); + const platform = os.platform(); + const architecture = os.arch(); + + console.log("Platform: " + platform); + console.log("Architecture: " + architecture); + + if (!(os.arch() in ARCHITECTURE_MAPPING) || !(os.platform() in PLATFORM_MAPPING)) { + throw new Error(`Installation is not supported for this ${platform}/${architecture} combination.`); + } + + const packageJsonPath = path.join(".", "package.json"); + + try { + const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent.toString()); + validatePackageConfig(packageJson); + + const packageName = packageJson.packageName; + const alias = packageJson.alias; + const binaries = packageJson.binaries; + const extension = platform === "win32" ? ".exe" : ""; + const binaryKey = `${packageName}-${platform}-${architecture}`; + const binaryPath = binaries[binaryKey]; + + if (binaryPath) { + console.log(`Binary found for your platform ${platform}-${architecture}: ${binaryPath}`); + } else { + throw new Error(`No binary found for your platform ${platform}-${architecture}.`); + } + + return { + alias, + binaryKey, + binaryPath, + extension + }; + } catch (error) { + throw new Error("Unable to read or parse package.json. Please run this script at the root of the package you want to be installed."); + } +} + + +async function createSymlink(binarySource, binaryDestination) { + const globalBinPath = getGlobalBinPath(); + const symlinkPath = path.join(globalBinPath, binaryDestination); + + try { + try { + await fs.access(symlinkPath, fs.constants.F_OK); + // If the symlink exists, remove it. + await fs.unlink(symlinkPath); + console.log(`Existing symlink ${symlinkPath} found and removed.`); + } catch (error) { + // The symlink does not exist, continue. + } + + if (os.platform() === "win32") { + // Create a junction for the binary for Windows. + // await fs.symlink(binarySource, symlinkPath, "junction"); + } else { + // Create a symlink for the binary for macOS and Linux. + await fs.symlink(binarySource, symlinkPath); + } + console.info(`Created symlink ${symlinkPath} pointing to ${binarySource}`); + } catch (error) { + console.error("Error while creating symlink:", error); + throw new Error("Failed to create symlink."); + } +} + +async function installCli() { + const config = await parsePackageJson(); + + console.log({ + config + }); + + console.info(`Creating symlink for the relevant binary for your platform ${os.platform()}-${os.arch()}`); + + const currentDir = __dirname; + const targetDir = path.join(currentDir, '..'); + const binarySource = path.join(targetDir, config.binaryPath); + const binaryDestination = config.alias; + + console.info("Installing cli:", binarySource, binaryDestination); + + await createSymlink(binarySource, binaryDestination + config.extension); +} + +async function uninstallCli() { + const config = await parsePackageJson(); + + try { + const globalBinPath = getGlobalBinPath(); + const symlinkPath = path.join(globalBinPath, config.alias + config.extension); + + console.info("Removing symlink:", symlinkPath); + + await fs.unlink(symlinkPath); + } catch (ex) { + console.error("Error while uninstalling:", ex); + } + + console.info("Uninstalled cli successfully"); +} + +const actions = { + "install": installCli, + "uninstall": uninstallCli +}; + +const [cmd] = process.argv.slice(2); +if (cmd && actions[cmd]) { + actions[cmd]().then( + () => process.exit(0), + (err) => { + console.error(err); + process.exit(1); + } + ); +} else { + console.log("Invalid command. `install` and `uninstall` are the only supported commands"); + process.exit(1); +} \ No newline at end of file